{"record":{"id":"b6eea1298723ed0a","repo":"BornToBeRoot/NETworkManager","slug":"additional-ldap-filter-must-be-a-valid-ldap-expression","errorCode":null,"errorMessage":"Additional LDAP filter must be a valid LDAP expression starting with '(' and ending with ')'.","messagePattern":"Additional LDAP filter must be a valid LDAP expression starting with '\\(' and ending with '\\)'\\.","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"Source/NETworkManager.Utilities/ActiveDirectory/ActiveDirectoryComputerSearcher.cs","lineNumber":144,"sourceCode":"    ///     If an <see cref=\"ActiveDirectorySearchOptions.AdditionalFilter\"/> is supplied it is AND-combined\n    ///     with the base filter.\n    /// </summary>\n    /// <exception cref=\"InvalidOperationException\">\n    ///     Thrown when <see cref=\"ActiveDirectorySearchOptions.AdditionalFilter\"/> is not a valid LDAP\n    ///     expression (must start with <c>(</c> and end with <c>)</c>).\n    /// </exception>\n    private static string BuildLdapFilter(ActiveDirectorySearchOptions options)\n    {\n        var baseFilter = options.ExcludeDisabledAccounts\n            ? \"(&(objectCategory=computer)(objectClass=computer)(!(userAccountControl:1.2.840.113556.1.4.803:=2)))\"\n            : \"(&(objectCategory=computer)(objectClass=computer))\";\n\n        var additional = options.AdditionalFilter?.Trim();\n        if (string.IsNullOrEmpty(additional))\n            return baseFilter;\n\n        if (!additional.StartsWith('(') || !additional.EndsWith(')'))\n            throw new InvalidOperationException(\n                \"Additional LDAP filter must be a valid LDAP expression starting with '(' and ending with ')'.\");\n\n        return $\"(&{baseFilter}{additional})\";\n    }\n\n    /// <summary>\n    ///     Returns <see langword=\"true\"/> when <paramref name=\"value\"/> begins with a recognised\n    ///     LDAP protocol prefix (<c>LDAP://</c>, <c>LDAPS://</c>, or <c>GC://</c>), indicating\n    ///     that the search base already contains a fully-qualified path.\n    /// </summary>\n    private static bool StartsWithProtocol(string value)\n    {\n        return value.StartsWith(\"LDAP://\", StringComparison.OrdinalIgnoreCase) ||\n               value.StartsWith(\"LDAPS://\", StringComparison.OrdinalIgnoreCase) ||\n               value.StartsWith(\"GC://\", StringComparison.OrdinalIgnoreCase);\n    }\n\n    /// <summary>","sourceCodeStart":126,"sourceCodeEnd":162,"githubUrl":"https://github.com/BornToBeRoot/NETworkManager/blob/2780d65469917a296dbc15f206107d72e2d0115c/Source/NETworkManager.Utilities/ActiveDirectory/ActiveDirectoryComputerSearcher.cs#L126-L162","documentation":"BuildLdapFilter composes the base computer filter with an optional user-supplied AdditionalFilter. Because the additional filter is inserted verbatim into an LDAP AND expression (&...), it must itself be a parenthesized LDAP expression; otherwise the resulting filter would be malformed, so the method throws InvalidOperationException.","triggerScenarios":"Setting ActiveDirectorySearchOptions.AdditionalFilter to a value that, after trimming, does not start with '(' or end with ')' — e.g. 'objectClass=computer', '(objectClass=computer) AND (name=a*)', or an empty expression built dynamically.","commonSituations":"Users entering raw LDAP attribute conditions without parentheses in the settings UI; concatenating multiple filters without wrapping each in parens; copying a full filter string that already has an outer (&...) wrapper.","solutions":["Wrap the additional filter in parentheses: 'objectClass=computer' -> '(objectClass=computer)'","Combine multiple conditions as a single parenthesized expression: '(&(objectClass=computer)(name=a*))'","Strip any redundant outer AND wrapper so the string starts with '(' and ends with ')'","Trim whitespace before validation (the code trims, but stray inner characters still break LDAP parsing)"],"exampleFix":"// before\noptions.AdditionalFilter = \"objectClass=computer\";\n// after\noptions.AdditionalFilter = \"(objectClass=computer)\";","handlingStrategy":"validation","validationCode":"var additional = options.AdditionalFilter?.Trim();\nbool valid = string.IsNullOrEmpty(additional) || (additional.StartsWith('(') && additional.EndsWith(')'));\nif (!valid) throw new ArgumentException(\"Additional filter must be a parenthesized LDAP expression.\");","typeGuard":"bool IsValidLdapFragment(string f) { var t = f?.Trim(); return string.IsNullOrEmpty(t) || (t.StartsWith('(') && t.EndsWith(')')); }","tryCatchPattern":"try { var computers = searcher.GetComputersInSubtree(path); }\ncatch (InvalidOperationException ex) when (ex.Message.Contains(\"Additional LDAP filter\")) { /* surface a validation message to fix the filter setting */ }","preventionTips":["Always write additional filters as '(attr=value)'","Never append multiple bare conditions; wrap combined expressions in '(&...)'","Validate user-supplied filter strings at input time","Test composed filters with a tool like ldp.exe"],"tags":["ldap","validation","filter"],"backgroundTag":"invalid-argument-format","analyzedSha":"2780d65469917a296dbc15f206107d72e2d0115c","analyzedAt":"2026-09-12T17:00:17.986Z","contentChangedAt":"2026-09-12T17:00:17.986Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}