Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Dec 2, 2025

Add a built-in EnvironmentBoundary component for Blazor

  • You've read the Contributor Guide and Code of Conduct.
  • You've included unit or integration tests for your change, where applicable.
  • You've included inline docs for your change, where applicable.
  • There's an open issue for the PR that you are making. If you'd like to propose a new feature or change, please open an issue to discuss the change or find an existing issue.

Add EnvironmentBoundary component for conditional rendering based on hosting environment

Description

Adds a built-in EnvironmentBoundary component similar to the MVC environment tag helper. Currently, Blazor developers must inject IHostEnvironment or IWebAssemblyHostEnvironment and write conditional logic manually—which is error-prone for components that run on both Server and WebAssembly.

Changes

  • New EnvironmentBoundary component (Microsoft.AspNetCore.Components.Web.EnvironmentBoundary)

    • Include: comma-separated environments to render in
    • Exclude: comma-separated environments to suppress rendering (takes precedence)
    • Case-insensitive matching, follows MVC EnvironmentTagHelper semantics
    • Consistent behavior with MVC tag helper when environment name is null/empty
    • Located in Components.Web assembly
  • WebAssembly support

    • Added internal WebAssemblyHostEnvironmentAdapter implementing IHostEnvironment
    • Registered automatically in WebAssembly DI container
    • Microsoft.Extensions.Hosting.Abstractions reference added to Components.Web (flows transitively to dependent projects)
  • Unit and E2E tests

    • Unit tests in Microsoft.AspNetCore.Components.Web.Tests
    • E2E test component (EnvironmentBoundaryContainer.razor) in BasicTestApp
    • E2E tests in EnvironmentBoundaryTest.cs with proper environment detection and element existence assertions

Usage

<EnvironmentBoundary Include="Development">
    <div class="alert">Debug mode enabled</div>
</EnvironmentBoundary>

<EnvironmentBoundary Include="Development, Staging">
    <p>Pre-production environment</p>
</EnvironmentBoundary>

<EnvironmentBoundary Exclude="Production">
    <p>@DateTime.Now</p>
</EnvironmentBoundary>
Original prompt

This section details on the original issue you should resolve

<issue_title>Add a built-in Environment component</issue_title>
<issue_description>MVC & Razor Pages have an environment tag helper for rendering content based on the current environment. In Blazor, you have to inject either the IHostEnvironment or IWebAssemblyHostEnvironment services and then write conditional rendering logic. This is tricky to do correctly for components that run on WebAssembly and are prerendered because you don't have a common way to get the environment that works for server and client. We have complicated explanations in the Blazor docs on how to handle this. We should consider just adding an Environment component, similar to the environment tag helper, that makes it easy to render content based on the current environment.</issue_description>

<agent_instructions>Here's a sketch of the Environment component implementation:

Implementation

// Licensed to the .NET Foundation under one or more agreements.
// The . NET Foundation licenses this file to you under the MIT license. 

using Microsoft.Extensions.Hosting;

namespace Microsoft.AspNetCore.Components;

/// <summary>
/// A component that renders its child content only when the current hosting environment
/// matches one of the specified environment names.
/// </summary>
public sealed class Environment : ComponentBase
{
    private static readonly char[] NameSeparator = [','];

    [Inject]
    private IHostEnvironment HostEnvironment { get; set; } = default!;

    /// <summary>
    /// Gets or sets a comma-separated list of environment names in which the content should be rendered. 
    /// If the current environment is also in the <see cref="Exclude"/> list, the content will not be rendered.
    /// </summary>
    [Parameter]
    public string? Include { get; set; }

    /// <summary>
    /// Gets or sets a comma-separated list of environment names in which the content will not be rendered.
    /// </summary>
    [Parameter]
    public string? Exclude { get; set; }

    /// <summary>
    /// Gets or sets the content to be rendered when the environment matches. 
    /// </summary>
    [Parameter]
    public RenderFragment?  ChildContent { get; set; }

    /// <inheritdoc />
    protected override void BuildRenderTree(RenderTreeBuilder builder)
    {
        if (ShouldRenderContent())
        {
            builder.AddContent(0, ChildContent);
        }
    }

    private bool ShouldRenderContent()
    {
        var currentEnvironmentName = HostEnvironment. EnvironmentName?. Trim();

        if (string.IsNullOrEmpty(currentEnvironmentName))
        {
            return false;
        }

        // Check exclusions first - if current environment is excluded, don't render
        if (! string.IsNullOrWhiteSpace(Exclude))
        {
            foreach (var environment in ParseEnvironmentNames(Exclude))
            {
                if (environment. Equals(currentEnvironmentName, StringComparison.OrdinalIgnoreCase))
                {
                    return false;
                }
            }
        }

        // If no inclusions specified, render (unless excluded above)
        if (string.IsNullOrWhiteSpace(Include))
        {
            return true;
        }

        // Check if current environment is in the include list
        foreach (var environment in ParseEnvironmentNames(Include))
        {
            if (environment. Equals(currentEnvironmentName, StringComparison.OrdinalIgnoreCase))
            {
                return true;
            }
        }

        return false;
    }

    private static IEnumerable<string> ParseEnvironmentNames(string names)
    {
        foreach (var segment in names.Split(NameSeparator, StringSplitOptions.RemoveEmptyEntries))
        {
            var trimmed = segment.Trim();
            if (trimmed.Length > 0)
            {
                yield return trimmed;
            }
        }
    }
}

Usage Examples

@* Show content only in Development *@
<Environment Include="Development">
    <div class="alert alert-warning">
        You are running in Development mode.  Debug features are enabled. 
    </div>
</Environment>

@* Show content in Development or Staging *@
<Environment Include="Development, Staging">
    <p>This is a pre-production environment.</p>
</Environment>

@* Show content in all environments except Production *@
<Environment Exclude="Production">
    <p>Debug information: @DateTime.Now</p>
</Environment>

Key Design Decisions

  1. Uses IHostEnvironment from Microsoft.Extensions. Hosting.Abstractions - available in both Server and WebAssembly hosting scenarios
  2. Follows EnvironmentTagHelper semantics - Include/Exclude parameter names and behavior match the existing MVC tag helper. Exclude takes precedence over Include.
  3. Case-insensitive comparison - matches the tag helper behavior
  4. Simplified API - uses just Include instead...

💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

@dotnet-policy-service
Copy link
Contributor

Greetings! You've submitted a PR that modifies code that is shared with https://github.com/dotnet/runtime . Please make sure you synchronize this code with the changes in that repo!

Copilot AI and others added 2 commits December 2, 2025 23:03
Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com>
…sembly adapter

Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com>
Copilot AI changed the title [WIP] Add a built-in Environment component for rendering Add a built-in Environment component for Blazor Dec 2, 2025
Copilot finished work on behalf of javiercn December 2, 2025 23:10
Copilot AI requested a review from javiercn December 2, 2025 23:10
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot This should live in the Components.Web assembly, not in the Components assembly directly. Also, the name is not great. As an exception, we can name it EnvironmentBoundary to begin with. We will decide on the final name later. As a result of this rename, there will be many unnecessary changes. Make sure you undo those. Also follow the guidance in src/Components/AGENTS.md to add an E2E test for it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved the component to Components.Web assembly as EnvironmentBoundary, reverted unnecessary System.Environment changes, and added E2E test assets. Commit: 0b21f28

…nd add E2E tests

Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com>
Copilot AI changed the title Add a built-in Environment component for Blazor Add a built-in EnvironmentBoundary component for Blazor Dec 3, 2025
Copilot AI requested a review from javiercn December 3, 2025 14:44
Copilot finished work on behalf of javiercn December 3, 2025 14:44
@javiercn javiercn added the area-blazor Includes: Blazor, Razor Components label Dec 3, 2025
Copy link
Member

@javiercn javiercn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot some more feedback.

<Reference Include="Microsoft.AspNetCore.Components" />
<Reference Include="Microsoft.AspNetCore.Components.Web" />
<Reference Include="Microsoft.Extensions.DependencyInjection" />
<Reference Include="Microsoft.Extensions.Hosting.Abstractions" />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this needed since it comes in from Components.Web transitively?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed. It comes transitively from Components.Web. Commit: 4b1e0c7

Comment on lines 9 to 11
/// <summary>
/// An implementation of <see cref="IHostEnvironment"/> that wraps <see cref="IWebAssemblyHostEnvironment"/>.
/// </summary>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// <summary>
/// An implementation of <see cref="IHostEnvironment"/> that wraps <see cref="IWebAssemblyHostEnvironment"/>.
/// </summary>

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the XML doc comment. Commit: 4b1e0c7

<Reference Include="Microsoft.AspNetCore.Components.Web" />
<Reference Include="Microsoft.Extensions.Configuration.Json" />
<Reference Include="Microsoft.Extensions.Configuration.Binder" />
<Reference Include="Microsoft.Extensions.Hosting.Abstractions" />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this needed since it will come in from Components.Web?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed. It comes transitively from Components.Web. Commit: 4b1e0c7

@javiercn javiercn marked this pull request as ready for review December 3, 2025 16:09
@javiercn javiercn requested a review from a team as a code owner December 3, 2025 16:09
Copilot AI review requested due to automatic review settings December 3, 2025 16:09
Copilot finished reviewing on behalf of javiercn December 3, 2025 16:14
… and XML doc comments

Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com>
Copilot finished work on behalf of javiercn December 3, 2025 16:17
Copilot AI requested a review from javiercn December 3, 2025 16:17
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a new EnvironmentBoundary component to Blazor for conditional rendering based on the hosting environment, addressing the need for a unified approach that works across both Server and WebAssembly scenarios. The implementation follows the MVC EnvironmentTagHelper semantics with Include and Exclude parameters for environment-based rendering control.

Key Changes

  • Added EnvironmentBoundary component in Microsoft.AspNetCore.Components.Web with case-insensitive environment matching
  • Implemented WebAssembly support via WebAssemblyHostEnvironmentAdapter that bridges IWebAssemblyHostEnvironment to IHostEnvironment
  • Added comprehensive unit tests and E2E tests covering various environment matching scenarios

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/Components/Web/src/EnvironmentBoundary.cs Core component implementation with Include/Exclude logic and environment name parsing
src/Components/Web/src/Microsoft.AspNetCore.Components.Web.csproj Added reference to Microsoft.Extensions.Hosting.Abstractions
src/Components/Web/src/PublicAPI.Unshipped.txt Public API surface additions for EnvironmentBoundary
src/Components/Web/test/EnvironmentBoundaryTest.cs Comprehensive unit tests covering matching logic, case-insensitivity, and edge cases
src/Components/Web/test/Microsoft.AspNetCore.Components.Web.Tests.csproj Added reference to Microsoft.Extensions.Hosting.Abstractions for tests
src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostEnvironmentAdapter.cs Adapter implementing IHostEnvironment by wrapping IWebAssemblyHostEnvironment
src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs Registered IHostEnvironment adapter in WebAssembly DI container
src/Components/WebAssembly/WebAssembly/src/Microsoft.AspNetCore.Components.WebAssembly.csproj Added reference to Microsoft.Extensions.Hosting.Abstractions
src/Components/test/testassets/BasicTestApp/EnvironmentBoundaryContainer.razor Test component demonstrating various usage patterns
src/Components/test/E2ETest/Tests/EnvironmentBoundaryTest.cs E2E tests verifying component behavior in Development environment

var currentEnvironmentName = HostEnvironment.EnvironmentName?.Trim();

if (string.IsNullOrEmpty(currentEnvironmentName))
{
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The behavior when EnvironmentName is null or empty differs from the MVC EnvironmentTagHelper. The MVC tag helper renders content when the environment name is not set (lines 82-86 of EnvironmentTagHelper.cs return early without suppressing output). This component returns false (hiding content) in the same scenario.

For consistency with the existing MVC tag helper, consider changing this to return true (render content) when the environment name is null/empty and no Include/Exclude are specified. Alternatively, if hiding content is the desired behavior, this should be documented as an intentional deviation from the MVC tag helper.

Suggested change
{
{
// For consistency with MVC EnvironmentTagHelper, render content when environment name is not set
// and no Include/Exclude are specified.
if (string.IsNullOrWhiteSpace(Include) && string.IsNullOrWhiteSpace(Exclude))
{
return true;
}

Copilot uses AI. Check for mistakes.
Copy link
Member

@javiercn javiercn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot seems like you have failing E2E tests.

Check failure on line 90 in Build log

@azure-pipelines
azure-pipelines
/ aspnetcore-components-e2e
Build log #L90

Bash exited with code '1'.
Check failure on line 1 in Microsoft.AspNetCore.Components.E2ETest.Tests.EnvironmentBoundaryTest.DisplaysCurrentEnvironment

@azure-pipelines
azure-pipelines
/ aspnetcore-components-e2e
Microsoft.AspNetCore.Components.E2ETest.Tests.EnvironmentBoundaryTest.DisplaysCurrentEnvironment

OpenQA.Selenium.NoSuchElementException : Cannot locate option with value: BasicTestApp.EnvironmentBoundaryContainer; For documentation on this error, please visit: https://www.selenium.dev/documentation/webdriver/troubleshooting/errors#nosuchelementexception
Check failure on line 1 in Microsoft.AspNetCore.Components.E2ETest.Tests.EnvironmentBoundaryTest.RendersContentWhenEnvironmentMatches

@azure-pipelines
azure-pipelines
/ aspnetcore-components-e2e
Microsoft.AspNetCore.Components.E2ETest.Tests.EnvironmentBoundaryTest.RendersContentWhenEnvironmentMatches

OpenQA.Selenium.NoSuchElementException : Cannot locate option with value: BasicTestApp.EnvironmentBoundaryContainer; For documentation on this error, please visit: https://www.selenium.dev/documentation/webdriver/troubleshooting/errors#nosuchelementexception
Check failure on line 1 in Microsoft.AspNetCore.Components.E2ETest.Tests.EnvironmentBoundaryTest.HidesContentWhenEnvironmentDoesNotMatch

@azure-pipelines
azure-pipelines
/ aspnetcore-components-e2e
Microsoft.AspNetCore.Components.E2ETest.Tests.EnvironmentBoundaryTest.HidesContentWhenEnvironmentDoesNotMatch

OpenQA.Selenium.NoSuchElementException : Cannot locate option with value: BasicTestApp.EnvironmentBoundaryContainer; For documentation on this error, please visit: https://www.selenium.dev/documentation/webdriver/troubleshooting/errors#nosuchelementexception

Follow the instructions in src/Components/Agents.md to manually validate the scenarios and run the test locally until you fix them.

…d align null environment behavior with MVC

Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Dec 3, 2025

@copilot seems like you have failing E2E tests.

Check failure on line 90 in Build log ...

Fixed. Added EnvironmentBoundaryContainer to the BasicTestApp Index.razor dropdown menu. The component was created but not registered in the test selector. Commit: d2cbc231

Copilot AI requested a review from javiercn December 3, 2025 22:55
Copilot finished work on behalf of javiercn December 3, 2025 22:55
Copy link
Member

@javiercn javiercn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot tests still failing.

@azure-pipelines
azure-pipelines
/ aspnetcore-components-e2e
Microsoft.AspNetCore.Components.E2ETest.Tests.EnvironmentBoundaryTest.HidesContentWhenEnvironmentDoesNotMatch

OpenQA.Selenium.BrowserAssertFailedException : Xunit.Sdk.EmptyException: Assert.Empty() Failure: Collection was not empty
Collection: [Element (id = f.042FF0182B862ADB4C4BCFB0313DA977.d.B0FF95B3D479CBC4752C29CB2AB8BDFB.e.13)]
at Xunit.Assert.Empty(IEnumerable collection) in /_/src/xunit.assert/Asserts/CollectionAsserts.cs:line 466
at Microsoft.AspNetCore.E2ETesting.WaitAssert.<>c__DisplayClass21_0`1.b__0(IWebDriver _) in /home/vsts/work/1/s/src/Shared/E2ETesting/WaitAssert.cs:line 109
Screen shot captured at '/home/vsts/work/1/s/src/Components/test/E2ETest/bin/screenshots/9d7e83583dd047d5869f298bb238d6bf.png'
Page content:

<base href="/subdir/">
<link href="style.css" rel="stylesheet">
<link rel="icon" href="data:,">

<!-- Used by ExternalContentPackage -->
<link href="_content/TestContentPackage/styles.css" rel="stylesheet">

<!-- App bundle that contains a reference to the scoped css bundle created by TestContentPackage -->
<link href="BasicTestApp.styles.css" rel="stylesheet">
<title>Basic test app</title>

Select test: Choose... Add/remove child components After-render interop component Async event handlers Auth cases Cascading authentication state bind cases Cascading values Circuit context Component ref component Concurrent rendering Configuration Configure runtime Content-editable Counter Counter using child component Counter wrapped in parent Culture Picker Custom elements data-* attribute rendering Dispatching to sync context Duplicate attributes DynamicComponent rendering Element focus component Element ref component Environment boundary Error throwing https://www.selenium.dev/documentation/webdriver/troubleshooting/errors#nosuchelementexception Run the E2E tests manually, debug what's going wrong and fix them.

…e assertions

Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Dec 4, 2025

@copilot tests still failing.

@azure-pipelines...

Fixed. Updated the E2E test to wait for the environment to be detected before asserting element absence, and switched to Browser.DoesNotExist for cleaner assertions. Commit: 8eda182

Copilot finished work on behalf of javiercn December 4, 2025 11:52
Copilot AI requested a review from javiercn December 4, 2025 11:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-blazor Includes: Blazor, Razor Components Attention: Shared Code Modified

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add a built-in Environment component

2 participants