Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 143 additions & 0 deletions src/Components/Web/src/EnvironmentBoundary.cs
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

Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.Extensions.Hosting;

namespace Microsoft.AspNetCore.Components.Web;

/// <summary>
/// A component that renders its child content only when the current hosting environment
/// matches one of the specified environment names.
/// </summary>
/// <remarks>
/// <para>
/// This component is similar to the environment tag helper in MVC and Razor Pages.
/// </para>
/// <example>
/// The following example shows how to conditionally render content based on the environment:
/// <code>
/// &lt;EnvironmentBoundary Include="Development"&gt;
/// &lt;div class="alert alert-warning"&gt;
/// You are running in Development mode. Debug features are enabled.
/// &lt;/div&gt;
/// &lt;/EnvironmentBoundary&gt;
///
/// &lt;EnvironmentBoundary Include="Development, Staging"&gt;
/// &lt;p&gt;This is a pre-production environment.&lt;/p&gt;
/// &lt;/EnvironmentBoundary&gt;
///
/// &lt;EnvironmentBoundary Exclude="Production"&gt;
/// &lt;p&gt;Debug information: @DateTime.Now&lt;/p&gt;
/// &lt;/EnvironmentBoundary&gt;
/// </code>
/// </example>
/// </remarks>
public sealed class EnvironmentBoundary : 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>
/// <remarks>
/// The specified environment names are compared case insensitively.
/// </remarks>
[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>
/// <remarks>
/// The specified environment names are compared case insensitively.
/// </remarks>
[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))
{
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.
// 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;
}

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
var hasEnvironments = false;
foreach (var environment in ParseEnvironmentNames(Include))
{
hasEnvironments = true;
if (environment.Equals(currentEnvironmentName, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}

// If Include was specified but contained no valid environments, render content
// (same behavior as when Include is not specified)
if (!hasEnvironments)
{
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;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
<Reference Include="Microsoft.AspNetCore.Components" />
<Reference Include="Microsoft.AspNetCore.Components.Forms" />
<Reference Include="Microsoft.Extensions.DependencyInjection" />
<Reference Include="Microsoft.Extensions.Hosting.Abstractions" />
<Reference Include="Microsoft.Extensions.Primitives" />
<Reference Include="Microsoft.JSInterop" />
</ItemGroup>
Expand Down
8 changes: 8 additions & 0 deletions src/Components/Web/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
#nullable enable
Microsoft.AspNetCore.Components.Web.EnvironmentBoundary
Microsoft.AspNetCore.Components.Web.EnvironmentBoundary.ChildContent.get -> Microsoft.AspNetCore.Components.RenderFragment?
Microsoft.AspNetCore.Components.Web.EnvironmentBoundary.ChildContent.set -> void
Microsoft.AspNetCore.Components.Web.EnvironmentBoundary.EnvironmentBoundary() -> void
Microsoft.AspNetCore.Components.Web.EnvironmentBoundary.Exclude.get -> string?
Microsoft.AspNetCore.Components.Web.EnvironmentBoundary.Exclude.set -> void
Microsoft.AspNetCore.Components.Web.EnvironmentBoundary.Include.get -> string?
Microsoft.AspNetCore.Components.Web.EnvironmentBoundary.Include.set -> void
Microsoft.AspNetCore.Components.Web.Media.FileDownload
Microsoft.AspNetCore.Components.Web.Media.FileDownload.ChildContent.get -> Microsoft.AspNetCore.Components.RenderFragment<Microsoft.AspNetCore.Components.Web.Media.FileDownloadContext!>?
Microsoft.AspNetCore.Components.Web.Media.FileDownload.ChildContent.set -> void
Expand Down
Loading
Loading