.NET 10 LDAP VlvRequestControl Validation: Stop Sending Replacement Bytes
When I audit an LDAP migration, .NET 10 LDAP VlvRequestControl validation is the kind of small compatibility change I want covered by a focused test. On .NET 9, an unpaired UTF-16 surrogate in a virtual-list-view target can be encoded as the UTF-8 replacement sequence EF BF BD . On .NET 10, the same malformed target is rejected with EncoderFallbackException before LDAP I/O. That stricter behavior is safer because a client no longer sends bytes that differ from the string it was given. It is still a breaking change if an application has been allowing malformed strings to reach VlvRequestControl . Why .NET 10 LDAP VlvRequestControl validation matters LDAP virtual list view controls let a client request a window around a target in a sorted result set. The VlvRequestControl API accepts that target as a .NET string, which must be converted from UTF-16 to UTF-8 when the control value is serialized. A valid surrogate pair represents one Unicode scalar value and should encode normally. A lone high surrogate such as \uD800 , or a lone low surrogate such as \uDC00 , is not valid Unicode text. The older fallback behavior substitutes U+FFFD , whose UTF-8 bytes are EF BF BD . That keeps serialization moving, but it silently changes the VLV target and may seek to an unintended position. Microsoft documents the strict encoding behavior in the .NET 10 LDAP compatibility note. I treat the resulting exception as input validation surfacing at the correct boundary, not as an LDAP server failure. Build a deterministic multi-target probe I use one project targeting both runtimes, with package versions pinned per target. That keeps the comparison explicit: System.DirectoryServices.Protocols 9.0.19 for net9.0 , and the stable System.DirectoryServices.Protocols 10.0.11 package for net10.0 . Exe net9.0;net10.0 enable enable The probe covers four inputs: plain ASCII, a valid surrogate pair, an unpaired high surrogate, and an unpaired low surrogate. It calls GetValue() directly, so it exercises the control's BER serialization without opening an LDAP connection. For a successful encoding, it also checks the serialized bytes for EF BF BD . using System.DirectoryServices.Protocols; using System.Text; var cases = new[] { (Name: "ASCII", Target: "smith"), (Name: "Valid pair", Target: "\uD83D\uDE80"), (Name: "Unpaired high", Target: "\uD800"), (Name: "Unpaired low", Target: "\uDC00") }; foreach (var item in cases) { try { var control = new VlvRequestControl(0, 0, item.Target); var encoded = control.GetValue(); var hasReplacement = Convert.ToHexString(encoded) .Contains("EFBFBD", StringComparison.Ordinal); Console.WriteLine( $"{item.Name}: encoded; replacement={hasReplacement}"); } catch (EncoderFallbackException) { Console.WriteLine($"{item.Name}: EncoderFallbackException"); } } The repository sample turns those observations into six assertions per target rather than relying on visual inspection. Its verifier runs both targets twice and also checks that repeated output is byte-for-byte identical: dotnet restore .\LdapVlvValidation.csproj --nologo dotnet build .\LdapVlvValidation.csproj --configuration Release --no-restore --nologo powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File ".\verify.ps1" The verifier finishes with Summary: 5/5 passed . No directory, credentials, or runtime network access is required. The complete runnable sample is on main , and the focused change is recorded in the merged pull request. Turn the result into a migration guardrail The expected split is precise. Both targets accept ASCII and the valid surrogate pair without replacement bytes. The .NET 9 target accepts each unpaired surrogate and the serialized control contains EF BF BD . The .NET 10 target rejects each malformed value with EncoderFallbackException . That makes this test useful during a .NET 9 to .NET 10 LDAP migration. If the modern target fails, the application has exposed malformed UTF-16 that used to be rewritten silently. I would trace the value back to its source and validate there, while retaining the boundary test so a future refactor cannot restore permissive behavior accidentally. I would not "fix" the exception by replacing invalid code units myself. Replacement still changes the requested target. If the input comes from an API, queue, file, or user interface, rejecting it with a clear validation error preserves the distinction between invalid text and an LDAP connectivity problem. Where I own the caller, I catch EncoderFallbackException at the validation boundary, translate it into a domain-level input error, and keep it out of generic retry logic. Retrying cannot repair malformed text, and treating the exception as transient can hide which record or request produced the value. I also avoid logging the malformed value itself; a safe source identifier and the validation category are enough to diagnose the path. Limitations and when not to use this check This sample verifies local control serialization only. It does not prove that a directory server supports virtual list view, that the server's sort rules match application expectations, or that paging and authorization are configured correctly. Those concerns need integration tests against the actual directory environment. It also targets a narrow compatibility boundary. If an application never constructs VlvRequestControl , this specific regression test adds little value. A broader Unicode-validation policy may be a better investment for systems accepting text from many sources. For applications that do use VLV targets, though, the offline test is fast, deterministic, and credential-free. It identifies the exact runtime change and keeps malformed input from being mistaken for a server-side LDAP issue. What edge-case strings are you adding to your .NET 10 migration tests? Happy testing! Top comments (0)
Comments
No comments yet. Start the discussion.