.NET GeneratedRegex culture handling can silently produce the wrong case-insensitive matches when two declarations use the same pattern and options but specify different cultures. On the affected .NET 10 generator, the second declaration can reuse the first declaration’s generated implementation, so a Turkish regex behaves like an English one without throwing an exception.

The safe response is to test the culture-sensitive characters your application accepts, search for declarations that differ only by cultureName, and keep those declarations on distinct pattern text until a released generator proves the original shape correct. This guide provides an executed detector, a verified containment pattern, and a CI gate you can remove only after testing the exact SDK and runtime you ship.

Recognize the silent culture collision

GeneratedRegexAttribute accepts a cultureName for case-insensitive comparisons. That value matters for the Turkish and Azerbaijani forms of the letter i. Under English rules, uppercase I is a case-insensitive match for lowercase i. Under Turkish rules, lowercase i pairs with uppercase dotted İ, while uppercase I pairs with lowercase dotless ı.

The reported generator defect loses that distinction in two places. First, generated implementations are deduplicated by pattern, options, and timeout without including the declared culture. Two declarations that differ only by culture can therefore share whichever implementation was generated first. Second, patterns that fall back to new Regex(...) can use the process culture at type initialization instead of the declared culture.

A successful build is not evidence that the culture contract is intact. The generated regex returns an ordinary Boolean, so the wrong answer can pass through normal application logic.

This matters anywhere the match controls a decision rather than presentation: identifier validation, routing, policy names, imported data, protocol tokens, search filters, or normalization before persistence. The defect does not mean every source-generated regex is unsafe. The high-risk shape is a case-insensitive declaration that relies on a specific culture, especially when another declaration uses the same pattern and options with a different culture.

Run a .NET GeneratedRegex culture detector

Create a plain net10.0 console project and add two declarations with identical pattern text. Set the process culture to en-US so the result does not depend on the machine’s default settings.

using System.Globalization;
using System.Text.RegularExpressions;

CultureInfo.CurrentCulture =
    CultureInfo.GetCultureInfo("en-US");

Print("en-US", Patterns.EnUs);
Print("tr-TR", Patterns.TrTr);

static void Print(string name, Regex regex) =>
    Console.WriteLine(
        $"{name}: I={regex.IsMatch("I")}, " +
        $"İ={regex.IsMatch("\u0130")}");

static partial class Patterns
{
    [GeneratedRegex("^i$", RegexOptions.IgnoreCase, "en-US")]
    public static partial Regex EnUs { get; }

    [GeneratedRegex("^i$", RegexOptions.IgnoreCase, "tr-TR")]
    public static partial Regex TrTr { get; }
}

Run the project in Release mode and record both the SDK and runtime:

dotnet run -c Release
dotnet --info
dotnet --list-runtimes

The expected Turkish result is I=False and İ=True. The task-local proof used SDK 10.0.401 and Microsoft.NETCore.App 10.0.12 on Linux x64. Both declarations returned I=True and İ=True, which means the Turkish declaration used English-style case equivalence.

DeclarationExpected for “I”Expected for “İ”Executed result
en-UStruetruetrue / true
tr-TRfalsetruetrue / true — affected

Do not reduce the detector to one ASCII input. Its purpose is to compare the culture-sensitive equivalence classes. At minimum, cover i, I, İ, and ı, then add the real strings your application accepts or rejects.

Separate culture-specific generator keys

For the deduplication path, keep the regular-expression language equivalent while making the pattern text distinct. A noncapturing group is enough for this small pattern:

static partial class Patterns
{
    [GeneratedRegex("^i$", RegexOptions.IgnoreCase, "en-US")]
    public static partial Regex EnUs { get; }

    // Equivalent language, distinct generator key.
    [GeneratedRegex("^(?:i)$", RegexOptions.IgnoreCase, "tr-TR")]
    public static partial Regex TrTr { get; }
}

The same task-local environment produced the correct Turkish result after this change: I=False and İ=True. This containment works because the generator no longer considers the declarations identical. It is not a promise that every textual rewrite is harmless, so review anchors, captures, backreferences, and options before changing a production pattern.

Prefer the smallest semantic no-op and explain it in code review. Avoid adding an unused capturing group because that changes the capture collection. Do not “fix” the problem by removing cultureName; that replaces an explicit rule with process-dependent behavior. If invariant matching is the actual business requirement, use RegexOptions.CultureInvariant and document that decision instead of pretending it is Turkish or English matching.

Treat generator fallback separately

The distinct-pattern workaround addresses the reported deduplication collision. It does not automatically repair generator fallback. Some patterns cannot be emitted as a specialized generated implementation, including certain non-backtracking patterns, deep trees, and case-insensitive backreferences. In those cases the generated type can construct a normal Regex and the declared culture may not reach that constructor path.

Inspect build diagnostics such as SYSLIB1044 and, when the decision is sensitive, inspect the emitted source in a controlled build. If the fallback must use invariant behavior, add RegexOptions.CultureInvariant and test it. If the application truly needs a specific non-invariant culture on a fallback-only pattern, keep that code out of the affected source-generator path until an appropriate released fix exists; a process-wide culture change is too broad to use as a production workaround.

<PropertyGroup>
  <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
  <CompilerGeneratedFilesOutputPath>
    $(BaseIntermediateOutputPath)generated
  </CompilerGeneratedFilesOutputPath>
</PropertyGroup>

Generated source is diagnostic evidence, not the final gate. Keep a behavior test because implementation names and emitted structure can change between SDK releases while the application contract stays the same.

Audit a real codebase

Start with a bounded text search, then group declarations by pattern, options, timeout, and culture:

rg -n 'GeneratedRegex\(' --glob '*.cs' src tests

rg -n 'RegexOptions\.IgnoreCase|cultureName'   --glob '*.cs' src tests

Flag two categories for review. The first is a duplicate generator key: identical pattern, options, and timeout with different cultures. The second is a culture-sensitive declaration whose pattern can fall back to new Regex(...). A text search finds candidates, but it cannot understand constants, partial classes, or option expressions. For a large repository, promote the check to a Roslyn-based test or analyzer rather than relying on a fragile regular expression.

  1. Identify every case-insensitive GeneratedRegex with a non-empty cultureName.
  2. Find declarations that share pattern, options, and timeout but differ in culture.
  3. Run the four-character culture matrix for each affected pattern family.
  4. Apply a reviewed semantic no-op only to declarations that collide.
  5. Record the SDK used to generate code and the runtime used to execute tests.

Audit generated code in dependencies only when the application actually consumes the behavior. If a library owns the declarations, prefer a fixed library release or an application-level validation boundary over copying generated internals into your codebase.

Keep a culture matrix in CI

A useful regression test asserts the contract instead of comparing generated source text:

[Theory]
[InlineData("i", true)]
[InlineData("İ", true)]
[InlineData("I", false)]
[InlineData("ı", false)]
public void Turkish_pattern_uses_Turkish_case_rules(
    string value,
    bool expected)
{
    Assert.Equal(expected, Patterns.TrTr.IsMatch(value));
}

Run the test with the exact SDK that generates the application and with every deployment target that matters. Source generators execute at build time, so changing only the runtime image may not change emitted code. Conversely, rebuilding with a different SDK can change generated behavior even when the target framework remains net10.0.

Use two pipeline lanes when upgrades are staged. The shipping lane retains the workaround and must remain green. An upgrade-probe lane restores the original identical pattern text and tests a candidate SDK. Remove the workaround only when that original shape passes the culture matrix on every supported build lane and the released version is recorded in the change.

Know the evidence limits

The independent proof covers the deduplication path on SDK 10.0.401, runtime 10.0.12, Linux x64, an en-US process culture, and the pattern ^i$. It also verifies that the equivalent ^(?:i)$ pattern restores the expected Turkish result in that environment.

The upstream report adds .NET 11 release-candidate observations and several fallback shapes. Those cases were not independently executed here. The evidence does not establish every culture, pattern, operating system, self-contained build, Native AOT build, or future servicing SDK. It also does not identify a released fixed version. Keep claims and removal decisions tied to the exact build environment you test.

Treat this .NET GeneratedRegex culture check as a version-pinned deployment guard, not as proof about configurations that the matrix did not execute.

References

Found this useful? Support more practical developer content.

Author

Practical .NET, Angular, Azure, Blazor, and AI engineering for real-world development.

Write A Comment

Ads Blocker Image Powered by Code Help Pro

Ads Blocker Detected!!!

We have detected that you are using extensions to block ads. Please support us by disabling these ads blocker.

Powered By
Best Wordpress Adblock Detecting Plugin | CHP Adblock