Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
d98fd2f
Add Unix shell profile modification for defaultinstall
dsplaisted Apr 13, 2026
f600b95
Include dotnetup directory in PATH in generated env scripts
dsplaisted Mar 13, 2026
b22597c
Implement defaultinstall admin on Unix with dotnetup-only profiles
dsplaisted Apr 13, 2026
99e2359
Rewrite unix-environment-setup.md to lead with user-facing workflows
dsplaisted Mar 15, 2026
d76fcd0
Remove no-op SetEnvironmentVariable calls on Unix
dsplaisted Apr 13, 2026
f9f50ba
Use eval consistently in print-env-script examples
dsplaisted Mar 15, 2026
a2dc117
Remove misleading 'source this script' comments from generated env sc…
dsplaisted Mar 15, 2026
30e041c
Update script examples to include dotnetup directory in PATH
dsplaisted Mar 15, 2026
c0369df
Update future work: defaultinstall admin is implemented
dsplaisted Mar 15, 2026
f9228c6
Skip default install prompt when shell is unsupported on Unix
dsplaisted Apr 13, 2026
9115b46
Consolidate shell provider list into ShellDetection
dsplaisted Apr 13, 2026
f498262
Merge AddProfileEntries and ReplaceProfileEntries into one method
dsplaisted Apr 13, 2026
406b1bf
Use single MarkerComment constant from ShellProfileManager
dsplaisted Mar 15, 2026
418ca64
Clear shell command cache in generated env scripts
dsplaisted Mar 15, 2026
2db5bcc
Inline ShellOption validators and completions into initializer
dsplaisted Mar 15, 2026
e5e071f
Move shell types to Microsoft.DotNet.Tools.Bootstrapper.Shell namespace
dsplaisted Apr 13, 2026
30353d5
Fix E2E test PATH assertion to handle dotnetup directory in PATH
dsplaisted Mar 15, 2026
cbf43ba
Fix post-rebase namespace fallout
dsplaisted Apr 13, 2026
1b15b8a
Align Unix shell setup APIs after rebase
dsplaisted Apr 13, 2026
82c8340
Fix code style issues
dsplaisted Apr 14, 2026
2143ad3
Preserve custom dotnetup install paths
dsplaisted Apr 14, 2026
2e20591
Don't pass --default-install-path to print-env-script unless necessary
dsplaisted Apr 14, 2026
9eb7acf
Misc fixes
dsplaisted Apr 15, 2026
3d921b2
Fix dotnetup Unix install root detection
dsplaisted Apr 15, 2026
9c8a286
Fix dotnetup Unix shell profile setup
dsplaisted Apr 15, 2026
05f12f1
Merge release/dnup into dotnetup-edit-profile
dsplaisted Apr 16, 2026
469e1e1
Address dotnetup shell feedback
dsplaisted Apr 17, 2026
ec74344
Keep shell override on init
dsplaisted Apr 17, 2026
fb3e47f
Clarify shell provider resolution
dsplaisted Apr 17, 2026
72bdbbc
Preserve shell profile formatting
dsplaisted Apr 17, 2026
0b32540
Simplify shell profile state usage
dsplaisted Apr 17, 2026
5e8ae65
Fix formatting
dsplaisted Apr 17, 2026
41c0aaa
Merge branch 'release/dnup' into dotnetup-edit-profile
nagilson Apr 17, 2026
f181c8f
Make difference between profile and environment modifications clearer
dsplaisted Apr 17, 2026
e37ba1c
Switch to begin / end markers for profile files
dsplaisted Apr 19, 2026
e5ecf33
Guard dotnetup profile entries
dsplaisted Apr 19, 2026
5cd9eb3
Add additional profile script line ending coverage
dsplaisted Apr 19, 2026
6baddc4
Add comment per PR review feedback
dsplaisted Apr 19, 2026
4ce4f3b
Use atomic shell profile updates
dsplaisted Apr 19, 2026
06eef24
Fix tests
dsplaisted Apr 19, 2026
390787f
Fix dotnetup init shell-profile install root
dsplaisted Apr 19, 2026
a7508d0
Fix defaultinstall user on Unix
dsplaisted Apr 19, 2026
919a1a3
Fix pwsh profile activation script
dsplaisted Apr 19, 2026
bcd8a1b
Refactor shell activation helpers
dsplaisted Apr 19, 2026
676a295
Align pwsh profile and activation output
dsplaisted Apr 19, 2026
44b57e0
Fix test and style issue
dsplaisted Apr 20, 2026
d9c3260
Fix default-install Unix profile generation
dsplaisted Apr 21, 2026
7601613
Fail safely on malformed profile blocks
dsplaisted Apr 21, 2026
3f698c0
Address shell profile review feedback
dsplaisted Apr 21, 2026
66f8747
Merge branch 'release/dnup' into dotnetup-edit-profile
nagilson Apr 21, 2026
638361d
Apply code review feedback
dsplaisted Apr 21, 2026
3437ba2
Isolate defaultinstall test environment
dsplaisted Apr 21, 2026
5cbb0a2
Remove unnecessary using
dsplaisted Apr 22, 2026
a0a64ce
Remove shebang from env scripts
dsplaisted Apr 22, 2026
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
214 changes: 143 additions & 71 deletions documentation/general/dotnetup/unix-environment-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,106 @@

## Overview

This document describes the design for setting up the .NET environment via initialization scripts using the `dotnetup print-env-script` command. This is the first step toward enabling automatic user profile configuration for Unix as described in [issue #51582](https://github.com/dotnet/sdk/issues/51582). Note that this also supports PowerShell and thus Windows, but on Windows the main method of configuring the environment will be to set environment variables which are stored in the registry instead of written by initialization scripts.
dotnetup (upon user consent) configures the Unix shell environment so that .NET is available in every new terminal session. This involves modifying shell profile files to set the `PATH` and `DOTNET_ROOT` environment variables. The same mechanism also supports PowerShell on any platform.

## Background
This document focuses on the Unix (and PowerShell) profile-based approach. On Windows, registry-based environment variables also impact the environment in addition to profile files.

The dotnetup tool manages multiple .NET installations in a local user hive. For .NET to be accessible from the command line, the installation directory must be:
1. Added to the `PATH` environment variable
2. Set as the `DOTNET_ROOT` environment variable
## How the Environment Gets Configured

On Unix systems, this requires modifying shell configuration files (like `.bashrc`, `.zshrc`, etc.) or sourcing environment setup scripts.
There are two primary ways the environment is configured:

## Design Goals
### 1. During `dotnetup sdk install` / `dotnetup runtime install`

1. **Non-invasive**: Don't automatically modify user shell configuration files without explicit consent
2. **Flexible**: Support multiple shells (bash, zsh, PowerShell)
3. **Reversible**: Users should be able to easily undo environment changes
4. **Single-file execution**: Generate scripts that can be sourced or saved for later use
5. **Discoverable**: Make it easy for users to understand how to configure their environment
When running interactively (the default in a terminal) **and no explicit `--install-path` is provided**, the install commands flow through the walkthrough. The walkthrough asks how the user wants to use dotnetup (for example, keeping it isolated vs. configuring the shell profile so `dotnet` works directly in new terminals).

## The `dotnetup print-env-script` Command
Choosing the shell-profile option in the walkthrough is what corresponds to making the dotnetup-managed user install the default:

- **On Windows**: Environment variables are set in the registry and updated for the current process.
- **On Unix**: Shell profile files are modified so .NET is available in future terminal sessions. Since profile changes only take effect in new shells, dotnetup also prints an activation command for the current terminal:

```
To start using .NET in this terminal, run:
eval "$('/home/user/.local/share/dotnetup/dotnetup' print-env-script --shell bash)"
```

If the user already has a saved path preference, or if the command is non-interactive / uses an explicit `--install-path`, the walkthrough prompt is skipped and dotnetup uses the existing configuration or the explicit path directly. If shell auto-detection is wrong or unavailable, run `dotnetup init --shell bash|zsh|pwsh` (or `defaultinstall` / `print-env-script` with `--shell`) before installing.

### 2. `dotnetup defaultinstall`

A standalone command that explicitly configures (or reconfigures) the default .NET install:

```bash
# Set up user-level default install (modifies shell profiles)
dotnetup defaultinstall user

# Switch to system-managed .NET (removes DOTNET_ROOT from profiles, keeps dotnetup on PATH)
dotnetup defaultinstall system
```

**`defaultinstall user`** on Unix:
1. Detects the current shell
2. Modifies the appropriate shell profile files
3. Prints an activation command for the current terminal

**`defaultinstall system`** on Unix:
- Replaces existing profile entries with dotnetup-only entries (keeps dotnetup on PATH but removes `DOTNET_ROOT` and dotnet from `PATH`), since the system package manager owns the .NET installation.

## Shell Profile Modification

### Which Profile Files Are Modified
Comment thread
dsplaisted marked this conversation as resolved.

| Shell | Files modified | Rationale |
|-------|---------------|-----------|
| **bash** | `~/.bashrc` (always) + the first existing of `~/.bash_profile` / `~/.profile` (creates `~/.profile` if neither exists) | `.bashrc` covers Linux terminals (non-login shells). The login profile covers macOS Terminal and SSH sessions. We never create `~/.bash_profile` to avoid shadowing an existing `~/.profile`. |
| **zsh** | `$ZDOTDIR/.zshrc` when `ZDOTDIR` is set; otherwise `~/.zshrc` (created if needed) | Covers all interactive zsh sessions. `~/.zshenv` is avoided because on macOS, `/etc/zprofile` runs `path_helper` which resets PATH after `.zshenv` loads. |
| **pwsh** | `~/.config/powershell/Microsoft.PowerShell_profile.ps1` (creates directory and file if needed) | Standard PowerShell profile path on Unix. |

The home directory used for these lookups comes from the user's current environment (`HOME`, or `USERPROFILE` / `Environment.SpecialFolder.UserProfile` as a fallback). dotnetup fails with a clear error if it cannot determine a writable profile location.

### Profile Entry Format

Each profile file gets a dotnetup-managed block with explicit begin/end markers:

**Bash / Zsh:**
```bash
# dotnetup: begin
if [ -x '/path/to/dotnetup' ]; then
eval "$('/path/to/dotnetup' print-env-script --shell bash)"
fi
# dotnetup: end
```

**PowerShell:**
```powershell
# dotnetup: begin
if (Test-Path -LiteralPath '/path/to/dotnetup' -PathType Leaf)
{
$dotnetupScript = & '/path/to/dotnetup' print-env-script --shell pwsh | Out-String
if (-not [string]::IsNullOrWhiteSpace($dotnetupScript))
{
Invoke-Expression $dotnetupScript
}
}
# dotnetup: end
```

The path to dotnetup is the full path to the running binary (`Environment.ProcessPath`). The `--dotnet-install-path` argument is only included in generated profile entries when dotnetup is configured to use a non-default install root.

### Safe updates

When updating an existing profile file, dotnetup writes the new content to a separate file and then swaps it into place. This avoids leaving a partially written profile behind if the update is interrupted, and it does not keep a persistent backup file after a successful update.

### Reversibility

To remove the environment configuration manually, remove the full block from `# dotnetup: begin` through `# dotnetup: end` in each profile file.

### Idempotency

If a profile file already contains a dotnetup-managed block, the entry is updated in place rather than duplicated.

## The `print-env-script` Command

`print-env-script` is the low-level building block that generates shell-specific environment scripts. It is called internally by profile entries and activation commands, but can also be used standalone for custom setups, CI pipelines, or when you want to source the environment without modifying profile files.

### Command Structure

Expand All @@ -32,6 +113,7 @@ dotnetup print-env-script [--shell <shell>] [--dotnet-install-path <path>]

- `--shell` / `-s`: The target shell for which to generate the environment script
- Supported values: `bash`, `zsh`, `pwsh`
- The supported shell values are based on what `nvm` and `rustup` support today.
- Optional: If not specified, automatically detects the current shell from the `$SHELL` environment variable
- On Windows, defaults to PowerShell (`pwsh`)

Expand All @@ -40,14 +122,9 @@ dotnetup print-env-script [--shell <shell>] [--dotnet-install-path <path>]

Comment thread
dsplaisted marked this conversation as resolved.
### Usage Examples

#### Auto-detect current shell
```bash
dotnetup print-env-script
```

#### Generate and source in one command
#### Eval directly (one-time, current terminal only)
```bash
source <(dotnetup print-env-script)
eval "$(dotnetup print-env-script)"
```

#### Explicitly specify shell
Expand All @@ -59,97 +136,93 @@ dotnetup print-env-script --shell zsh
```bash
dotnetup print-env-script --shell bash > ~/.dotnet-env.sh
# Later, in .bashrc or manually:
source ~/.dotnet-env.sh
. ~/.dotnet-env.sh
```

#### Use custom installation path
```bash
dotnetup print-env-script --dotnet-install-path /opt/dotnet
```

## Generated Script Format
### Generated Script Format

The command generates shell-specific scripts that:
1. Set the `DOTNET_ROOT` environment variable to the installation path
2. Prepend the installation path to the `PATH` environment variable
3. Clear the shell's cached command location for `dotnet` to pick up the new PATH

### Bash/Zsh Example
**Bash/Zsh Example:**
```bash
#!/usr/bin/env bash
# This script configures the environment for .NET installed at /home/user/.local/share/dotnet
# Source this script to add .NET to your PATH and set DOTNET_ROOT
# This bash script configures the environment for .NET installed at /home/user/.local/share/dotnet

export DOTNET_ROOT='/home/user/.local/share/dotnet'
export PATH='/home/user/.local/share/dotnet':$PATH
export PATH='/home/user/.local/share/dotnetup':'/home/user/.local/share/dotnet':$PATH
hash -d dotnet 2>/dev/null
hash -d dotnetup 2>/dev/null
```

### PowerShell Example
**PowerShell Example:**
```powershell
# This script configures the environment for .NET installed at /home/user/.local/share/dotnet
# Source this script (dot-source) to add .NET to your PATH and set DOTNET_ROOT
# Example: . ./dotnet-env.ps1
# This PowerShell script configures the environment for .NET installed at /home/user/.local/share/dotnet

$env:DOTNET_ROOT = '/home/user/.local/share/dotnet'
$env:PATH = '/home/user/.local/share/dotnet' + [IO.Path]::PathSeparator + $env:PATH
$env:PATH = '/home/user/.local/share/dotnetup' + [IO.Path]::PathSeparator + '/home/user/.local/share/dotnet' + [IO.Path]::PathSeparator + $env:PATH
```

### Shell Detection

When `--shell` is not specified, the command automatically detects the current shell:

1. **On Unix**: Reads the `$SHELL` environment variable, resolves symlinks when possible, and extracts the shell name from the resulting path (for example `/bin/bash` → `bash`)
2. **On Windows**: Defaults to PowerShell (`pwsh`)

### Security Considerations

All installation paths are properly escaped to prevent shell injection vulnerabilities:
- **Bash/Zsh**: Uses single quotes with `'\''` escaping for embedded single quotes
- **PowerShell**: Uses single quotes with `''` escaping for embedded single quotes

This ensures that paths containing special characters, spaces, or shell metacharacters are handled safely.

## Implementation Details

### Provider Model

The implementation uses a provider model similar to `System.CommandLine.StaticCompletions`, making it easy to add support for additional shells in the future.
The implementation uses a provider model, making it easy to add support for additional shells in the future.

**Interface**: `IEnvShellProvider`
```csharp
public interface IEnvShellProvider
{
string ArgumentName { get; } // Shell name for CLI (e.g., "bash")
string Extension { get; } // File extension (e.g., "sh")
string? HelpDescription { get; } // Help text for the shell
string GenerateEnvScript(string dotnetInstallPath);
string ArgumentName { get; }
string Extension { get; }
string? HelpDescription { get; }
string GenerateEnvScript(string dotnetInstallPath, string? dotnetupDir = null, bool includeDotnet = true);
IReadOnlyList<string> GetProfilePaths();
string GenerateProfileEntry(string dotnetupPath, bool dotnetupOnly = false, string? dotnetInstallPath = null);
string GenerateActivationCommand(string dotnetupPath, bool dotnetupOnly = false, string? dotnetInstallPath = null);
}
```

**Implementations**:
- `BashEnvShellProvider`: Generates bash-compatible scripts
- `ZshEnvShellProvider`: Generates zsh-compatible scripts
- `PowerShellEnvShellProvider`: Generates PowerShell Core scripts
**Implementations**: `BashEnvShellProvider`, `ZshEnvShellProvider`, `PowerShellEnvShellProvider`

### Shell Detection
### ShellDetection

The command automatically detects the current shell when the `--shell` option is not provided:
`ShellDetection.GetCurrentShellProvider()` resolves the user's current shell to the matching `IEnvShellProvider`. On Windows it returns the PowerShell provider; on Unix it reads `$SHELL`, resolves symlinks when possible, and allows callers to override detection with `--shell`.

1. **On Unix**: Reads the `$SHELL` environment variable and extracts the shell name from the path
- Example: `/bin/bash` → `bash`
2. **On Windows**: Defaults to PowerShell (`pwsh`)
### ShellProfileManager

### Security Considerations
`ShellProfileManager` coordinates profile file modifications:
- `AddProfileEntries(provider, dotnetupPath, dotnetupOnly, dotnetInstallPath)` — creates or updates the managed begin/end block in place, creates backups, and can thread through a custom install path
- `RemoveProfileEntries(provider)` — finds and removes the full managed block

**Path Escaping**: All installation paths are properly escaped to prevent shell injection vulnerabilities:
- **Bash/Zsh**: Uses single quotes with `'\''` escaping for embedded single quotes
- **PowerShell**: Uses single quotes with `''` escaping for embedded single quotes

This ensures that paths containing special characters, spaces, or shell metacharacters are handled safely.

## Advantages of Generated Scripts

As noted in the discussion, generating scripts dynamically has several advantages over using embedded resource files:

1. **Single-file execution**: Users can source the script directly from the command output without needing to extract files
2. **Flexibility**: Easy to customize the installation path or add future features
3. **No signing required**: Generated text doesn't require code signing, unlike downloaded executables or scripts
4. **Immediate availability**: No download or extraction step needed
5. **Transparency**: Users can easily inspect what the script does by running the command
`defaultinstall system` uses `AddProfileEntries(..., dotnetupOnly: true)` to switch the managed entry into dotnetup-only mode.

## Future Work

This command provides the foundation for future enhancements:

1. **Automatic profile modification**: Add a command to automatically update shell configuration files (`.bashrc`, `.zshrc`, etc.) with user consent
2. **Profile backup**: Create backups of shell configuration files before modification
3. **Uninstall/removal**: Add commands to remove dotnetup configuration from shell profiles
4. **Additional shells**: Support for fish, tcsh, and other shells
5. **Environment validation**: Commands to verify that the environment is correctly configured
1. **System-wide configuration on Unix**: Writing to system-wide locations like `/etc/profile.d/` for admin installs is not yet supported.
2. **Additional shells**: Support for fish, tcsh, and other shells.
3. **Environment validation**: Commands to verify that the environment is correctly configured.

## Related Issues

Expand All @@ -163,5 +236,4 @@ The implementation includes comprehensive tests:
- Shell provider tests for script generation
- Security tests for special character handling
- Help documentation tests

All tests ensure that the generated scripts are syntactically correct and properly escape paths.
- Shell profile manager tests for add/remove/idempotency/backup behavior
2 changes: 1 addition & 1 deletion src/Installer/.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -425,7 +425,7 @@ dotnet_diagnostic.IDE0019.severity = warning
dotnet_diagnostic.IDE0028.severity = warning

# IDE0305: Collection expression can be simplified
dotnet_diagnostic.IDE0305.severity = warning
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nit: why was this changed to a suggestion?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I'm used to calling ToList() at the end of a LINQ expression, and I wasn't sure the new syntax ([.. <LINQ expression>]) was necessarily simpler to understand.

dotnet_diagnostic.IDE0305.severity = suggestion

# IDE0032: Use auto-property
dotnet_diagnostic.IDE0032.severity = warning
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ public enum DotnetInstallErrorCode
/// <summary>The dotnetup installation manifest was modified externally and is now corrupted.</summary>
LocalManifestUserCorrupted,

/// <summary>A user-managed configuration file contains malformed dotnetup state.</summary>
UserConfigurationCorrupted,

/// <summary>The install path points to an existing file instead of a directory.</summary>
InstallPathIsFile,

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ public static bool IsValidChannelFormat(string channel)
}

// Check for prerelease suffix (e.g., "10.0.100-preview.1.32640")
var dashIndex = channel.IndexOf('-');
var dashIndex = channel.IndexOf('-', StringComparison.Ordinal);
var hasPrerelease = dashIndex >= 0;
var versionPart = hasPrerelease ? channel.Substring(0, dashIndex) : channel;

Expand Down Expand Up @@ -248,7 +248,7 @@ private static (int Major, int Minor, string? FeatureBand, bool IsFullySpecified

private static IEnumerable<Product> GetProductsInMajorOrMajorMinor(IEnumerable<Product> index, int major, int? minor = null)
{
var validProducts = index.Where(p => minor is not null ? p.ProductVersion.Equals($"{major}.{minor}") : p.ProductVersion.StartsWith($"{major}.", StringComparison.Ordinal));
var validProducts = index.Where(p => minor is not null ? p.ProductVersion.Equals($"{major}.{minor}", StringComparison.Ordinal) : p.ProductVersion.StartsWith($"{major}.", StringComparison.Ordinal));
return validProducts;
}

Expand Down Expand Up @@ -336,8 +336,8 @@ private static IEnumerable<Product> GetProductsInMajorOrMajorMinor(IEnumerable<P
private static int NormalizeFeatureBandInput(string band)
{
var bandString = band
.Replace("X", "x")
.Replace("x", "0")
.Replace("X", "x", StringComparison.Ordinal)
.Replace("x", "0", StringComparison.Ordinal)
.PadRight(3, '0')
.Substring(0, 3);
return int.Parse(bandString, CultureInfo.InvariantCulture);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ public static string ComputeFileHash(string filePath)
// Eventually the manifest should indicate which algorithm to use.
using var sha512 = SHA512.Create();
byte[] hashBytes = sha512.ComputeHash(fileStream);
return BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant();
return BitConverter.ToString(hashBytes).Replace("-", "", StringComparison.Ordinal).ToLowerInvariant();
}

// Known alternate acceptable hashes keyed by the manifest's expected hash.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ private static string DecompressTarGzIfNeeded(string archivePath, out bool needs
return archivePath;
}

string decompressedPath = archivePath.Replace(".gz", "");
string decompressedPath = archivePath.Replace(".gz", "", StringComparison.Ordinal);

using FileStream originalFileStream = File.OpenRead(archivePath);
using FileStream decompressedFileStream = File.Create(decompressedPath);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ private static (string normalized, bool isDirectoryEntry) NormalizeEntryPath(str
normalized = normalized.Substring(2);
}

bool isDirectoryEntry = normalized.EndsWith('/');
bool isDirectoryEntry = normalized.EndsWith('/', StringComparison.Ordinal);
Comment thread
dsplaisted marked this conversation as resolved.
return (normalized.TrimEnd('/'), isDirectoryEntry);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ private static bool IsValidVersionPattern(string version)
}

// Validate prerelease token: must start with a known token
var dotIndex = releaseVersion.Prerelease.IndexOf('.');
var dotIndex = releaseVersion.Prerelease.IndexOf('.', StringComparison.Ordinal);
var token = dotIndex < 0 ? releaseVersion.Prerelease : releaseVersion.Prerelease[..dotIndex];

return KnownPrereleaseTokens.Contains(token, StringComparer.OrdinalIgnoreCase);
Expand Down
Loading
Loading