{"record":{"id":"3a5877c604ce1c37","repo":"OrchardCMS/OrchardCore","slug":"lower-and-upper-bound-range-types-don-t-match","errorCode":null,"errorMessage":"Lower and upper bound range types don't match","messagePattern":"Lower and upper bound range types don't match","errorType":"exception","errorClass":"ArgumentException","httpStatus":null,"severity":"error","filePath":"src/OrchardCore/OrchardCore.Lucene.Core/QueryProviders/Filters/RangeFilterProvider.cs","lineNumber":68,"sourceCode":"                            break;\n                        case \"lt\":\n                            lt = element.Value;\n                            nodeKind = lt.GetValueKind();\n                            break;\n                        case \"lte\":\n                            lt = element.Value;\n                            nodeKind = lt.GetValueKind();\n                            includeUpper = true;\n                            break;\n                        case \"boost\":\n                            boost = element.Value.Value<float>();\n                            break;\n                    }\n                }\n\n                if (gt is not null && lt is not null && gt.GetValueKind() != lt.GetValueKind())\n                {\n                    throw new ArgumentException(\"Lower and upper bound range types don't match\");\n                }\n\n                switch (nodeKind)\n                {\n                    case JsonValueKind.Number:\n                        if (gt.AsValue().TryGetValue<long>(out var minInt) &&\n                            lt.AsValue().TryGetValue<long>(out var maxInt))\n                        {\n                            rangeQuery = NumericRangeQuery.NewInt64Range(field, minInt, maxInt, includeLower, includeUpper);\n                        }\n                        else if (gt.AsValue().TryGetValue<double>(out var minFloat) &&\n                            lt.AsValue().TryGetValue<double>(out var maxFloat))\n                        {\n                            rangeQuery = NumericRangeQuery.NewDoubleRange(field, minFloat, maxFloat, includeLower, includeUpper);\n                        }\n                        else\n                        {\n                            throw new ArgumentException($\"Unsupported range value type: {type}\");","sourceCodeStart":50,"sourceCodeEnd":86,"githubUrl":"https://github.com/OrchardCMS/OrchardCore/blob/4306c0717fe573f6fca1b4955909ddab6a192807/src/OrchardCore/OrchardCore.Lucene.Core/QueryProviders/Filters/RangeFilterProvider.cs#L50-L86","documentation":"RangeFilterProvider.CreateFilteredQuery supports range filters with a lower bound (gt/gte) and an upper bound (lt/lte). When both bounds are present their JSON value kinds must match (both numbers, or both strings, etc.); if they differ, e.g. gt is a number and lt is a string, the provider throws ArgumentException(\"Lower and upper bound range types don't match\") because Lucene range queries need a consistent type for min and max.","triggerScenarios":"Calling CreateFilteredQuery with {\"range\": {\"Price\": {\"gt\": 10, \"lt\": \"100\"}}} or any mix like {\"gte\": \"2024-01-01\", \"lt\": 5} where the lower and upper bound JSON kinds differ (number vs string, number vs boolean).","commonSituations":"Dates supplied as strings on one bound but parsed as numbers on the other; one bound quoted and the other not when hand-editing JSON; dynamic query builders formatting only one bound; data migrated between string-typed and numeric-typed fields.","solutions":["Make both bounds the same JSON type: use {\"gt\": 10, \"lt\": 100} or {\"gt\": \"a\", \"lt\": \"b\"}, never a mix.","If bounds are dates, quote both: {\"gte\": \"2024-01-01\", \"lte\": \"2024-12-31\"}.","In dynamic query builders, coerce/serialize both bounds from the same source type before building the filter.","Validate that gt/gte and lt/lte have equal JsonValueKind before calling CreateFilteredQuery."],"exampleFix":"// before\n{\"range\": {\"Price\": {\"gt\": 10, \"lt\": \"100\"}}}\n// after\n{\"range\": {\"Price\": {\"gt\": 10, \"lt\": 100}}}","handlingStrategy":"validation","validationCode":"var obj = rangeValue.AsObject();\nbool hasGt = obj.TryGetPropertyValue(\"gt\", out var gt) || obj.TryGetPropertyValue(\"gte\", out gt);\nbool hasLt = obj.TryGetPropertyValue(\"lt\", out var lt) || obj.TryGetPropertyValue(\"lte\", out lt);\nif (hasGt && hasLt && gt.ValueKind != lt.ValueKind)\n{\n    throw new InvalidOperationException(\n        $\"Range bounds must share the same JSON type (got {gt.ValueKind} vs {lt.ValueKind}).\");\n}","typeGuard":"static bool BoundsKindMatch(JsonElement? gt, JsonElement? lt) =>\n    gt is null || lt is null || gt.GetValueKind() == lt.GetValueKind();","tryCatchPattern":"try\n{\n    var query = provider.CreateFilteredQuery(context);\n}\ncatch (ArgumentException ex) when (ex.Message.Contains(\"types don't match\"))\n{\n    _logger.LogError(ex, \"Mixed range bound types: {Json}\", filterJson);\n    throw new QueryValidationException(\"Range lower/upper bounds must be the same type.\", ex);\n}","preventionTips":["Quote date bounds consistently on both sides.","In dynamic builders, derive both bounds from one typed value.","Check gt/gte and lt/lte JsonValueKind equality before executing."],"tags":["lucene","query-filter","type-mismatch","range-query"],"backgroundTag":"type-mismatch","analyzedSha":"4306c0717fe573f6fca1b4955909ddab6a192807","analyzedAt":"2026-09-13T17:41:05.024Z","contentChangedAt":"2026-09-13T17:41:05.024Z","schemaVersion":2},"datasetVersion":"2026-09-15T23:17:13.987Z"}