| | | 1 | | using System.Collections; |
| | | 2 | | using System.Reflection; |
| | | 3 | | using NGql.Core.Abstractions; |
| | | 4 | | |
| | | 5 | | namespace NGql.Core.Extensions; |
| | | 6 | | |
| | | 7 | | internal static class QueryBlockObjectExtensions |
| | | 8 | | { |
| | | 9 | | internal static SortedDictionary<string, object> GetArguments(this QueryBlock queryBlock, bool isRootElement) |
| | | 10 | | { |
| | 432 | 11 | | var arguments = new SortedDictionary<string, object>(StringComparer.Ordinal); |
| | 432 | 12 | | CopyArguments(queryBlock, isRootElement, arguments); |
| | | 13 | | |
| | 432 | 14 | | if (isRootElement) |
| | | 15 | | { |
| | 207 | 16 | | AddMissingRootVariables(queryBlock, arguments); |
| | | 17 | | } |
| | 432 | 18 | | return arguments; |
| | | 19 | | } |
| | | 20 | | |
| | | 21 | | private static void CopyArguments(QueryBlock queryBlock, bool isRootElement, SortedDictionary<string, object> argume |
| | | 22 | | { |
| | 1248 | 23 | | foreach (var kvp in queryBlock.Arguments) |
| | | 24 | | { |
| | 192 | 25 | | var key = kvp.Value is Variable variable && isRootElement ? variable.Name : kvp.Key; |
| | 192 | 26 | | arguments[key] = kvp.Value; |
| | | 27 | | } |
| | 432 | 28 | | } |
| | | 29 | | |
| | | 30 | | private static void AddMissingRootVariables(QueryBlock queryBlock, SortedDictionary<string, object> arguments) |
| | | 31 | | { |
| | 657 | 32 | | foreach (var variable in queryBlock.Variables.Where(v => !ContainsVariableNamed(arguments, v.Name))) |
| | | 33 | | { |
| | 75 | 34 | | arguments[variable.Name] = variable; |
| | | 35 | | } |
| | 207 | 36 | | } |
| | | 37 | | |
| | | 38 | | private static bool ContainsVariableNamed(SortedDictionary<string, object> arguments, string name) |
| | 156 | 39 | | => arguments.Values.Any(v => v is Variable existing && existing.Name == name); |
| | | 40 | | |
| | | 41 | | /// <summary> |
| | | 42 | | /// Adds the given type properties into <see cref="QueryBlock.FieldsList"/> part of the query. |
| | | 43 | | /// </summary> |
| | | 44 | | /// <param name="block">The query block</param> |
| | | 45 | | /// <param name="path">The path to use</param> |
| | | 46 | | /// <param name="name">The name to use</param> |
| | | 47 | | /// <param name="alias">The alias to use</param> |
| | | 48 | | /// <typeparam name="T">The type to include</typeparam> |
| | | 49 | | public static void IncludeAtPath<T>(this QueryBlock block, string path, string name, string? alias = null) |
| | | 50 | | { |
| | | 51 | | // Use Span to avoid string allocations during path parsing |
| | 18 | 52 | | var pathSpan = path.AsSpan(); |
| | 18 | 53 | | var currentBlock = block; |
| | | 54 | | |
| | 51 | 55 | | while (!pathSpan.IsEmpty) |
| | | 56 | | { |
| | 33 | 57 | | var dotIndex = pathSpan.IndexOf('.'); |
| | | 58 | | ReadOnlySpan<char> currentSegment; |
| | | 59 | | |
| | 33 | 60 | | if (dotIndex == -1) |
| | | 61 | | { |
| | | 62 | | // Last segment |
| | 12 | 63 | | currentSegment = pathSpan; |
| | 12 | 64 | | pathSpan = ReadOnlySpan<char>.Empty; |
| | | 65 | | } |
| | | 66 | | else |
| | | 67 | | { |
| | | 68 | | // Extract current segment and advance |
| | 21 | 69 | | currentSegment = pathSpan[..dotIndex]; |
| | 21 | 70 | | pathSpan = pathSpan[(dotIndex + 1)..]; |
| | | 71 | | } |
| | | 72 | | |
| | | 73 | | // Skip empty segments |
| | 33 | 74 | | if (currentSegment.IsEmpty || currentSegment.IsWhiteSpace()) |
| | | 75 | | { |
| | | 76 | | continue; |
| | | 77 | | } |
| | | 78 | | |
| | | 79 | | // Convert to string only when creating the QueryBlock |
| | 27 | 80 | | var segmentString = currentSegment.ToString(); |
| | 27 | 81 | | var subQuery = new QueryBlock(segmentString); |
| | 27 | 82 | | currentBlock.AddField(subQuery); |
| | 27 | 83 | | currentBlock = subQuery; |
| | | 84 | | } |
| | | 85 | | |
| | 18 | 86 | | currentBlock.Include<T>(name, alias); |
| | 18 | 87 | | } |
| | | 88 | | |
| | | 89 | | /// <summary> |
| | | 90 | | /// Adds the given type properties into <see cref="QueryBlock.FieldsList"/> part of the query. |
| | | 91 | | /// </summary> |
| | | 92 | | /// <param name="block">The query block</param> |
| | | 93 | | /// <param name="name">The name to use</param> |
| | | 94 | | /// <param name="alias"></param> |
| | | 95 | | /// <returns>Query</returns> |
| | | 96 | | public static void Include<T>(this QueryBlock block, string name, string? alias = null) |
| | | 97 | | { |
| | 24 | 98 | | var properties = typeof(T).GetProperties() |
| | 72 | 99 | | .OrderBy(p => p.Name, StringComparer.OrdinalIgnoreCase) |
| | 24 | 100 | | .ToArray(); |
| | | 101 | | |
| | 24 | 102 | | var subQuery = new QueryBlock(name, alias: alias); |
| | | 103 | | |
| | 24 | 104 | | HandleProperties(subQuery, null, properties); |
| | | 105 | | |
| | 24 | 106 | | block.AddField(subQuery); |
| | 24 | 107 | | } |
| | | 108 | | |
| | | 109 | | /// <summary> |
| | | 110 | | /// Adds the given object properties into <see cref="QueryBlock.FieldsList"/> part of the query. |
| | | 111 | | /// </summary> |
| | | 112 | | /// <param name="block">The query block</param> |
| | | 113 | | /// <param name="obj">A value</param> |
| | | 114 | | /// <returns>Query</returns> |
| | | 115 | | public static void Include(this QueryBlock block, object obj) |
| | | 116 | | { |
| | 15 | 117 | | var type = obj.GetType(); |
| | 15 | 118 | | var properties = type.GetProperties() |
| | 33 | 119 | | .OrderBy(p => p.Name, StringComparer.OrdinalIgnoreCase) |
| | 15 | 120 | | .ToArray(); |
| | | 121 | | |
| | 15 | 122 | | HandleProperties(block, obj, properties); |
| | 15 | 123 | | } |
| | | 124 | | |
| | | 125 | | private static void HandleProperties(QueryBlock block, object? obj, PropertyInfo[] properties) |
| | | 126 | | { |
| | 288 | 127 | | foreach (var property in properties) |
| | | 128 | | { |
| | 105 | 129 | | HandleProperty(block, obj, property); |
| | | 130 | | } |
| | 39 | 131 | | } |
| | | 132 | | |
| | | 133 | | private static void HandleProperty(QueryBlock block, object? obj, PropertyInfo property) |
| | | 134 | | { |
| | 105 | 135 | | var value = obj is null ? null : property.GetValue(obj); |
| | 105 | 136 | | var alias = property.GetAlias(); |
| | | 137 | | |
| | 105 | 138 | | if (value is IDictionary dict) |
| | | 139 | | { |
| | 3 | 140 | | HandleDictionary(block, property.Name, alias, dict); |
| | 3 | 141 | | return; |
| | | 142 | | } |
| | 102 | 143 | | if (value is not null && !IsSimpleType(value.GetType())) |
| | | 144 | | { |
| | 6 | 145 | | var subQuery = new QueryBlock(property.Name, alias: alias); |
| | 6 | 146 | | subQuery.Include(value); |
| | 6 | 147 | | block.AddField(subQuery); |
| | 6 | 148 | | return; |
| | | 149 | | } |
| | 96 | 150 | | AddSimpleProperty(block, property.Name, alias); |
| | 96 | 151 | | } |
| | | 152 | | |
| | | 153 | | private static void AddSimpleProperty(QueryBlock block, string propertyName, string? alias) |
| | | 154 | | { |
| | 96 | 155 | | if (alias == propertyName) |
| | | 156 | | { |
| | 27 | 157 | | block.AddField(propertyName); |
| | 27 | 158 | | return; |
| | | 159 | | } |
| | 69 | 160 | | block.AddField(alias is null |
| | 69 | 161 | | ? new QueryBlock(propertyName) { IsEmpty = true } |
| | 69 | 162 | | : new QueryBlock(alias, alias: propertyName) { IsEmpty = true }); |
| | 69 | 163 | | } |
| | | 164 | | |
| | | 165 | | private static void HandleDictionary(QueryBlock block, string name, string? alias, IDictionary dict) |
| | | 166 | | { |
| | 6 | 167 | | var subQuery = new QueryBlock(name, alias: alias); |
| | 6 | 168 | | var sortedKeys = dict.Keys.Cast<object>() |
| | 18 | 169 | | .OrderBy(k => k.ToString(), StringComparer.OrdinalIgnoreCase); |
| | | 170 | | |
| | 36 | 171 | | foreach (var key in sortedKeys) |
| | | 172 | | { |
| | 12 | 173 | | if (dict[key] is IDictionary nestedDict) |
| | | 174 | | { |
| | 3 | 175 | | HandleDictionary(subQuery, key.ToString()!, null, nestedDict); |
| | | 176 | | } |
| | | 177 | | else |
| | | 178 | | { |
| | 9 | 179 | | subQuery.AddField(key.ToString()!); |
| | | 180 | | } |
| | | 181 | | } |
| | | 182 | | |
| | 6 | 183 | | block.AddField(subQuery); |
| | 6 | 184 | | } |
| | | 185 | | |
| | 3 | 186 | | private static readonly HashSet<Type> SimpleTypes = |
| | 3 | 187 | | [ |
| | 3 | 188 | | typeof(string), |
| | 3 | 189 | | typeof(decimal), |
| | 3 | 190 | | typeof(DateTime), |
| | 3 | 191 | | typeof(DateTimeOffset), |
| | 3 | 192 | | typeof(TimeSpan), |
| | 3 | 193 | | typeof(Guid) |
| | 3 | 194 | | ]; |
| | | 195 | | |
| | | 196 | | private static bool IsSimpleType(Type type) |
| | 27 | 197 | | => type.IsPrimitive || |
| | 27 | 198 | | SimpleTypes.Contains(type) || |
| | 27 | 199 | | Convert.GetTypeCode(type) != TypeCode.Object; |
| | | 200 | | } |
| | | 201 | | |