| | 1 | | // Copyright (c) Microsoft Corporation. All rights reserved. |
| | 2 | | // Licensed under the MIT License. |
| | 3 | |
|
| | 4 | | using System; |
| | 5 | | using System.Globalization; |
| | 6 | | using System.Text; |
| | 7 | | #if EXPERIMENTAL_SPATIAL |
| | 8 | | using Azure.Core; |
| | 9 | | using Azure.Core.Spatial; |
| | 10 | | #endif |
| | 11 | |
|
| | 12 | | namespace Azure.Search.Documents |
| | 13 | | { |
| | 14 | | /// <summary> |
| | 15 | | /// The SearchFilter class is used to help construct valid OData filter |
| | 16 | | /// expressions, like the kind used by <see cref="SearchOptions.Filter"/>, |
| | 17 | | /// by automatically replacing, quoting, and escaping interpolated |
| | 18 | | /// parameters. |
| | 19 | | /// For more information, see <see href="https://docs.microsoft.com/azure/search/search-filters">Filters in Azure Co |
| | 20 | | /// </summary> |
| | 21 | | public static class SearchFilter |
| | 22 | | { |
| | 23 | | /// <summary> |
| | 24 | | /// Create an OData filter expression from an interpolated string. The |
| | 25 | | /// interpolated values will be quoted and escaped as necessary. |
| | 26 | | /// </summary> |
| | 27 | | /// <param name="filter">An interpolated filter string.</param> |
| | 28 | | /// <returns>A valid OData filter expression.</returns> |
| | 29 | | public static string Create(FormattableString filter) => |
| 62 | 30 | | Create(filter, null); |
| | 31 | |
|
| | 32 | | /// <summary> |
| | 33 | | /// Create an OData filter expression from an interpolated string. The |
| | 34 | | /// interpolated values will be quoted and escaped as necessary. |
| | 35 | | /// </summary> |
| | 36 | | /// <param name="filter">An interpolated filter string.</param> |
| | 37 | | /// <param name="formatProvider"> |
| | 38 | | /// Format provider used to convert values to strings. |
| | 39 | | /// <see cref="CultureInfo.InvariantCulture"/> is used as a default. |
| | 40 | | /// </param> |
| | 41 | | /// <returns>A valid OData filter expression.</returns> |
| | 42 | | public static string Create(FormattableString filter, IFormatProvider formatProvider) |
| | 43 | | { |
| 0 | 44 | | if (filter == null) { return null; } |
| 62 | 45 | | formatProvider ??= CultureInfo.InvariantCulture; |
| | 46 | |
|
| 62 | 47 | | string[] args = new string[filter.ArgumentCount]; |
| 264 | 48 | | for (int i = 0; i < filter.ArgumentCount; i++) |
| | 49 | | { |
| 71 | 50 | | args[i] = filter.GetArgument(i) switch |
| 71 | 51 | | { |
| 71 | 52 | | // Null |
| 72 | 53 | | null => "null", |
| 71 | 54 | |
|
| 71 | 55 | | // Boolean |
| 75 | 56 | | bool x => x.ToString(formatProvider).ToLowerInvariant(), |
| 71 | 57 | |
|
| 71 | 58 | | // Numeric |
| 74 | 59 | | sbyte x => x.ToString(formatProvider), |
| 73 | 60 | | byte x => x.ToString(formatProvider), |
| 74 | 61 | | short x => x.ToString(formatProvider), |
| 73 | 62 | | ushort x => x.ToString(formatProvider), |
| 90 | 63 | | int x => x.ToString(formatProvider), |
| 73 | 64 | | uint x => x.ToString(formatProvider), |
| 74 | 65 | | long x => x.ToString(formatProvider), |
| 73 | 66 | | ulong x => x.ToString(formatProvider), |
| 75 | 67 | | decimal x => x.ToString(formatProvider), |
| 71 | 68 | |
|
| 71 | 69 | | // Floating point |
| 79 | 70 | | float x => JsonSerialization.Float(x, formatProvider), |
| 79 | 71 | | double x => JsonSerialization.Double(x, formatProvider), |
| 71 | 72 | |
|
| 71 | 73 | | // Dates as 8601 with a time zone |
| 72 | 74 | | DateTimeOffset x => JsonSerialization.Date(x, formatProvider), |
| 72 | 75 | | DateTime x => JsonSerialization.Date(x, formatProvider), |
| 71 | 76 | |
|
| 71 | 77 | | #if EXPERIMENTAL_SPATIAL |
| 71 | 78 | | // Points |
| 71 | 79 | | GeometryPosition x => EncodeGeometry(x), |
| 71 | 80 | | PointGeometry x => EncodeGeometry(x), |
| 71 | 81 | |
|
| 71 | 82 | | // Polygons |
| 71 | 83 | | LineGeometry x => EncodeGeometry(x), |
| 71 | 84 | | PolygonGeometry x => EncodeGeometry(x), |
| 71 | 85 | | #endif |
| 71 | 86 | |
|
| 71 | 87 | | // Text |
| 74 | 88 | | string x => Quote(x), |
| 74 | 89 | | char x => Quote(x.ToString(formatProvider)), |
| 72 | 90 | | StringBuilder x => Quote(x.ToString()), |
| 71 | 91 | |
|
| 71 | 92 | | // Everything else |
| 72 | 93 | | object x => throw new ArgumentException( |
| 72 | 94 | | $"Unable to convert argument {i} from type {x.GetType()} to an OData literal.") |
| 71 | 95 | | }; |
| | 96 | | } |
| 61 | 97 | | string text = string.Format(formatProvider, filter.Format, args); |
| 61 | 98 | | return text; |
| | 99 | | } |
| | 100 | |
|
| | 101 | | /// <summary> |
| | 102 | | /// Quote and escape OData strings. |
| | 103 | | /// </summary> |
| | 104 | | /// <param name="text">The text to quote.</param> |
| | 105 | | /// <returns>The quoted text.</returns> |
| | 106 | | private static string Quote(string text) |
| | 107 | | { |
| 0 | 108 | | if (text == null) { return "null"; } |
| | 109 | |
|
| | 110 | | // Optimistically allocate an extra 5% for escapes |
| 7 | 111 | | StringBuilder builder = new StringBuilder(2 + (int)(text.Length * 1.05)); |
| 7 | 112 | | builder.Append("'"); |
| 52 | 113 | | foreach (char ch in text) |
| | 114 | | { |
| 19 | 115 | | builder.Append(ch); |
| 19 | 116 | | if (ch == '\'') |
| | 117 | | { |
| 2 | 118 | | builder.Append(ch); |
| | 119 | | } |
| | 120 | | } |
| 7 | 121 | | builder.Append("'"); |
| 7 | 122 | | return builder.ToString(); |
| | 123 | | } |
| | 124 | |
|
| | 125 | | #if EXPERIMENTAL_SPATIAL |
| | 126 | | /// <summary> |
| | 127 | | /// Convert a <see cref="GeometryPosition"/> to an OData value. |
| | 128 | | /// </summary> |
| | 129 | | /// <param name="position">The position.</param> |
| | 130 | | /// <returns>The OData representation of the position.</returns> |
| | 131 | | private static string EncodeGeometry(GeometryPosition position) |
| | 132 | | { |
| | 133 | | const int maxLength = |
| | 134 | | 19 + // "geography'POINT( )'".Length |
| | 135 | | 2 * // Lat and Long each have: |
| | 136 | | (15 + // Maximum precision for a double (without G17) |
| | 137 | | 1 + // Optional decimal point |
| | 138 | | 1); // Optional negative sign |
| | 139 | | StringBuilder odata = new StringBuilder(maxLength); |
| | 140 | | odata.Append("geography'POINT("); |
| | 141 | | odata.Append(JsonSerialization.Double(position.Longitude, CultureInfo.InvariantCulture)); |
| | 142 | | odata.Append(" "); |
| | 143 | | odata.Append(JsonSerialization.Double(position.Latitude, CultureInfo.InvariantCulture)); |
| | 144 | | odata.Append(")'"); |
| | 145 | | return odata.ToString(); |
| | 146 | | } |
| | 147 | |
|
| | 148 | | /// <summary> |
| | 149 | | /// Convert a <see cref="PointGeometry"/> to an OData value. |
| | 150 | | /// </summary> |
| | 151 | | /// <param name="point">The point.</param> |
| | 152 | | /// <returns>The OData representation of the point.</returns> |
| | 153 | | private static string EncodeGeometry(PointGeometry point) |
| | 154 | | { |
| | 155 | | Argument.AssertNotNull(point, nameof(point)); |
| | 156 | | return EncodeGeometry(point.Position); |
| | 157 | | } |
| | 158 | |
|
| | 159 | | /// <summary> |
| | 160 | | /// Convert a <see cref="LineGeometry"/> forming a polygon to an OData |
| | 161 | | /// value. A LineGeometry must have at least four |
| | 162 | | /// <see cref="LineGeometry.Positions"/> and the first and last must |
| | 163 | | /// match to form a searchable polygon. |
| | 164 | | /// </summary> |
| | 165 | | /// <param name="line">The line forming a polygon.</param> |
| | 166 | | /// <returns>The OData representation of the line.</returns> |
| | 167 | | private static string EncodeGeometry(LineGeometry line) |
| | 168 | | { |
| | 169 | | Argument.AssertNotNull(line, nameof(line)); |
| | 170 | | Argument.AssertNotNull(line.Positions, $"{nameof(line)}.{nameof(line.Positions)}"); |
| | 171 | | if (line.Positions.Count < 4) |
| | 172 | | { |
| | 173 | | throw new ArgumentException( |
| | 174 | | $"A {nameof(LineGeometry)} must have at least four {nameof(LineGeometry.Positions)} to form a search |
| | 175 | | $"{nameof(line)}.{nameof(line.Positions)}"); |
| | 176 | | } |
| | 177 | | else if (line.Positions[0] != line.Positions[line.Positions.Count - 1]) |
| | 178 | | { |
| | 179 | | throw new ArgumentException( |
| | 180 | | $"A {nameof(LineGeometry)} must have matching first and last {nameof(LineGeometry.Positions)} to for |
| | 181 | | $"{nameof(line)}.{nameof(line.Positions)}"); |
| | 182 | | } |
| | 183 | |
|
| | 184 | | Argument.AssertInRange(line.Positions?.Count ?? 0, 4, int.MaxValue, $"{nameof(line)}.{nameof(line.Positions) |
| | 185 | |
|
| | 186 | | StringBuilder odata = new StringBuilder(); |
| | 187 | | odata.Append("geography'POLYGON(("); |
| | 188 | | bool first = true; |
| | 189 | | foreach (GeometryPosition position in line.Positions) |
| | 190 | | { |
| | 191 | | if (!first) { odata.Append(","); } |
| | 192 | | first = false; |
| | 193 | | odata.Append(JsonSerialization.Double(position.Longitude, CultureInfo.InvariantCulture)); |
| | 194 | | odata.Append(" "); |
| | 195 | | odata.Append(JsonSerialization.Double(position.Latitude, CultureInfo.InvariantCulture)); |
| | 196 | | } |
| | 197 | | odata.Append("))'"); |
| | 198 | | return odata.ToString(); |
| | 199 | | } |
| | 200 | |
|
| | 201 | | /// <summary> |
| | 202 | | /// Convert a <see cref="PolygonGeometry"/> to an OData value. A |
| | 203 | | /// PolygonGeometry must have exactly one <see cref="PolygonGeometry.Rings"/> |
| | 204 | | /// to form a searchable polygon. |
| | 205 | | /// </summary> |
| | 206 | | /// <param name="polygon">The polygon.</param> |
| | 207 | | /// <returns>The OData representation of the polygon.</returns> |
| | 208 | | private static string EncodeGeometry(PolygonGeometry polygon) |
| | 209 | | { |
| | 210 | | Argument.AssertNotNull(polygon, nameof(polygon)); |
| | 211 | | Argument.AssertNotNull(polygon.Rings, $"{nameof(polygon)}.{nameof(polygon.Rings)}"); |
| | 212 | | if (polygon.Rings.Count != 1) |
| | 213 | | { |
| | 214 | | throw new ArgumentException( |
| | 215 | | $"A {nameof(PolygonGeometry)} must have exactly one {nameof(PolygonGeometry.Rings)} to form a search |
| | 216 | | $"{nameof(polygon)}.{nameof(polygon.Rings)}"); |
| | 217 | | } |
| | 218 | | return EncodeGeometry(polygon.Rings[0]); |
| | 219 | | } |
| | 220 | | #endif |
| | 221 | | } |
| | 222 | | } |