├── .gitattributes ├── .gitignore ├── JsonQ ├── Exceptions │ └── QuerySyntaxErrorException.cs ├── ExpressionNode.cs ├── ExpressionOperator.cs ├── Extensions │ ├── DbContextExtensions.cs │ └── StringExtensions.cs ├── ILexer.cs ├── ISource.cs ├── JsonQ.csproj ├── JsonQueryableParser.cs ├── LambdaCompare.cs ├── Lexer.cs ├── LexerContext.cs ├── Location.cs ├── MethodCreator.cs ├── Parser.cs ├── ParserContext.cs ├── Properties │ └── AssemblyInfo.cs ├── QueryableCreator.cs ├── Source.cs └── Token.cs ├── JsonToQueryable.sln ├── LICENSE.txt └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | [Xx]64/ 19 | [Xx]86/ 20 | [Bb]uild/ 21 | bld/ 22 | [Bb]in/ 23 | [Oo]bj/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | artifacts/ 46 | 47 | *_i.c 48 | *_p.c 49 | *_i.h 50 | *.ilk 51 | *.meta 52 | *.obj 53 | *.pch 54 | *.pdb 55 | *.pgc 56 | *.pgd 57 | *.rsp 58 | *.sbr 59 | *.tlb 60 | *.tli 61 | *.tlh 62 | *.tmp 63 | *.tmp_proj 64 | *.log 65 | *.vspscc 66 | *.vssscc 67 | .builds 68 | *.pidb 69 | *.svclog 70 | *.scc 71 | 72 | # Chutzpah Test files 73 | _Chutzpah* 74 | 75 | # Visual C++ cache files 76 | ipch/ 77 | *.aps 78 | *.ncb 79 | *.opendb 80 | *.opensdf 81 | *.sdf 82 | *.cachefile 83 | *.VC.db 84 | 85 | # Visual Studio profiler 86 | *.psess 87 | *.vsp 88 | *.vspx 89 | *.sap 90 | 91 | # TFS 2012 Local Workspace 92 | $tf/ 93 | 94 | # Guidance Automation Toolkit 95 | *.gpState 96 | 97 | # ReSharper is a .NET coding add-in 98 | _ReSharper*/ 99 | *.[Rr]e[Ss]harper 100 | *.DotSettings.user 101 | 102 | # JustCode is a .NET coding add-in 103 | .JustCode 104 | 105 | # TeamCity is a build add-in 106 | _TeamCity* 107 | 108 | # DotCover is a Code Coverage Tool 109 | *.dotCover 110 | 111 | # NCrunch 112 | _NCrunch_* 113 | .*crunch*.local.xml 114 | nCrunchTemp_* 115 | 116 | # MightyMoose 117 | *.mm.* 118 | AutoTest.Net/ 119 | 120 | # Web workbench (sass) 121 | .sass-cache/ 122 | 123 | # Installshield output folder 124 | [Ee]xpress/ 125 | 126 | # DocProject is a documentation generator add-in 127 | DocProject/buildhelp/ 128 | DocProject/Help/*.HxT 129 | DocProject/Help/*.HxC 130 | DocProject/Help/*.hhc 131 | DocProject/Help/*.hhk 132 | DocProject/Help/*.hhp 133 | DocProject/Help/Html2 134 | DocProject/Help/html 135 | 136 | # Click-Once directory 137 | publish/ 138 | 139 | # Publish Web Output 140 | *.[Pp]ublish.xml 141 | *.azurePubxml 142 | 143 | # TODO: Un-comment the next line if you do not want to checkin 144 | # your web deploy settings because they may include unencrypted 145 | # passwords 146 | #*.pubxml 147 | *.publishproj 148 | 149 | # NuGet Packages 150 | *.nupkg 151 | # The packages folder can be ignored because of Package Restore 152 | **/packages/* 153 | # except build/, which is used as an MSBuild target. 154 | !**/packages/build/ 155 | # Uncomment if necessary however generally it will be regenerated when needed 156 | #!**/packages/repositories.config 157 | # NuGet v3's project.json files produces more ignoreable files 158 | *.nuget.props 159 | *.nuget.targets 160 | 161 | # Microsoft Azure Build Output 162 | csx/ 163 | *.build.csdef 164 | 165 | # Microsoft Azure Emulator 166 | ecf/ 167 | rcf/ 168 | 169 | # Windows Store app package directory 170 | AppPackages/ 171 | BundleArtifacts/ 172 | 173 | # Visual Studio cache files 174 | # files ending in .cache can be ignored 175 | *.[Cc]ache 176 | # but keep track of directories ending in .cache 177 | !*.[Cc]ache/ 178 | 179 | # Others 180 | ClientBin/ 181 | [Ss]tyle[Cc]op.* 182 | ~$* 183 | *~ 184 | *.dbmdl 185 | *.dbproj.schemaview 186 | *.pfx 187 | *.publishsettings 188 | node_modules/ 189 | orleans.codegen.cs 190 | 191 | # RIA/Silverlight projects 192 | Generated_Code/ 193 | 194 | # Backup & report files from converting an old project file 195 | # to a newer Visual Studio version. Backup files are not needed, 196 | # because we have git ;-) 197 | _UpgradeReport_Files/ 198 | Backup*/ 199 | UpgradeLog*.XML 200 | UpgradeLog*.htm 201 | 202 | # SQL Server files 203 | *.mdf 204 | *.ldf 205 | 206 | # Business Intelligence projects 207 | *.rdl.data 208 | *.bim.layout 209 | *.bim_*.settings 210 | 211 | # Microsoft Fakes 212 | FakesAssemblies/ 213 | 214 | # GhostDoc plugin setting file 215 | *.GhostDoc.xml 216 | 217 | # Node.js Tools for Visual Studio 218 | .ntvs_analysis.dat 219 | 220 | # Visual Studio 6 build log 221 | *.plg 222 | 223 | # Visual Studio 6 workspace options file 224 | *.opt 225 | 226 | # Visual Studio LightSwitch build output 227 | **/*.HTMLClient/GeneratedArtifacts 228 | **/*.DesktopClient/GeneratedArtifacts 229 | **/*.DesktopClient/ModelManifest.xml 230 | **/*.Server/GeneratedArtifacts 231 | **/*.Server/ModelManifest.xml 232 | _Pvt_Extensions 233 | 234 | # LightSwitch generated files 235 | GeneratedArtifacts/ 236 | ModelManifest.xml 237 | 238 | # Paket dependency manager 239 | .paket/paket.exe 240 | 241 | # FAKE - F# Make 242 | .fake/ 243 | -------------------------------------------------------------------------------- /JsonQ/Exceptions/QuerySyntaxErrorException.cs: -------------------------------------------------------------------------------- 1 | namespace JsonQ.Exceptions 2 | { 3 | using System; 4 | using System.Linq; 5 | 6 | public class QuerySyntaxErrorException : Exception 7 | { 8 | public QuerySyntaxErrorException(string description, ISource source, int location) 9 | : base(ComposeMessage(description, source, location)) 10 | { 11 | } 12 | 13 | private static string ComposeMessage(string description, ISource source, int loc) 14 | { 15 | var location = new Location(source, loc); 16 | 17 | return $"Syntax Error Query ({location.Line}:{location.Column}) {description}" + 18 | "\n" + HighlightSourceAtLocation(source, location); 19 | } 20 | 21 | private static string HighlightSourceAtLocation(ISource source, Location location) 22 | { 23 | var line = location.Line; 24 | var prevLineNum = (line - 1).ToString(); 25 | var lineNum = line.ToString(); 26 | var nextLineNum = (line + 1).ToString(); 27 | var padLen = nextLineNum.Length; 28 | var lines = source.Body 29 | .Split(new[] { "\n" }, StringSplitOptions.None) 30 | .Select(ReplaceWithUnicodeRepresentation) 31 | .ToArray(); 32 | 33 | return 34 | (line >= 2 ? LeftPad(padLen, prevLineNum) + ": " + lines[line - 2] + "\n" : string.Empty) + 35 | LeftPad(padLen, lineNum) + ": " + lines[line - 1] + "\n" + 36 | LeftPad(1 + padLen + location.Column, string.Empty) + "^" + "\n" + 37 | (line < lines.Length ? LeftPad(padLen, nextLineNum) + ": " + lines[line] + "\n" : string.Empty); 38 | } 39 | 40 | private static string LeftPad(int length, string str) 41 | { 42 | string pad = string.Empty; 43 | 44 | for (var i = 0; i < length - str.Length; i++) 45 | pad += " "; 46 | 47 | return pad + str; 48 | } 49 | 50 | private static string ReplaceWithUnicodeRepresentation(string str) 51 | { 52 | foreach (var code in str) 53 | { 54 | if (code < 0x0020 && code != 0x0009 && code != 0x000A && code != 0x000D) 55 | str = str.Replace(string.Empty + code, "\\u" + ((int)code).ToString("D4")); 56 | } 57 | 58 | return str; 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /JsonQ/ExpressionNode.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Linq.Expressions; 5 | using System.Reflection; 6 | 7 | namespace JsonQ 8 | { 9 | public class ExpressionNode 10 | { 11 | internal int Depth { get; } 12 | internal string NodeName { get; } 13 | internal bool IsEnumerable { get; } 14 | internal bool ShouldInclude { get; } 15 | internal Type Type { get; } 16 | internal ExpressionNode Parent { get; } 17 | internal ExpressionNode Root => Parent == null ? this : Parent.Root; 18 | internal ParameterExpression ParameterExpression { get; } 19 | internal Expression ComputedExpression { get; } 20 | internal Dictionary Properties { get; } 21 | internal IList Include { get; set; } 22 | /// 23 | /// For where clause 24 | /// 25 | internal Expression WherePredictExpression { get; private set; } 26 | 27 | internal ExpressionNode(Type type, string name, bool isEnumerable, bool include, ExpressionNode parent, Expression computedExpression = null) 28 | { 29 | Type = type; 30 | NodeName = name; 31 | ShouldInclude = include; 32 | Parent = parent; 33 | ComputedExpression = computedExpression; 34 | ParameterExpression = Expression.Parameter(type, type.Name); 35 | IsEnumerable = isEnumerable; 36 | Properties = new Dictionary(); 37 | Depth = parent?.Depth + 1 ?? 0; 38 | } 39 | 40 | internal Expression CreateWherePredictExpression() 41 | { 42 | //create where for object's properties first 43 | foreach (var subNode in Properties.Values) 44 | { 45 | if (subNode.ShouldInclude) 46 | { 47 | var expr = subNode.CreateWherePredictExpression(); 48 | subNode.WherePredictExpression = subNode.IsEnumerable ? subNode.CreateAnyExpression() : expr; 49 | } 50 | else 51 | { 52 | subNode.WherePredictExpression = subNode.ComputedExpression; 53 | } 54 | } 55 | 56 | var predicts = Properties.Values.Where(p => p.WherePredictExpression != null).Select(p => p.WherePredictExpression).ToList(); 57 | return predicts.Any() ? WherePredictExpression = predicts.Aggregate(Expression.AndAlso) : null; 58 | } 59 | 60 | internal Expression CreateIncludeExpression(Expression pExpression, bool hasncludeFilter) 61 | { 62 | //if this is the end of node chain 63 | if (Include == null || Include.Count == 0) 64 | { 65 | var exp = CreateIncludeMethodCallExpression(pExpression, hasncludeFilter); 66 | return Properties.Values.Where(v => v.ShouldInclude).Aggregate(exp, (expression, node) => (MethodCallExpression)node.CreateIncludeExpression(expression, hasncludeFilter)); 67 | } 68 | 69 | var result = pExpression; 70 | 71 | foreach (var node in Properties.Values.Where(p => p.ShouldInclude)) 72 | { 73 | var exp = CreateIncludeMethodCallExpression(pExpression, hasncludeFilter); 74 | result = node.CreateIncludeExpression(exp, hasncludeFilter); 75 | } 76 | 77 | return result; 78 | } 79 | 80 | private MethodCallExpression CreateIncludeMethodCallExpression(Expression pExpression, bool hasncludeFilter) 81 | { 82 | MethodInfo include; 83 | Expression includePropertyExpr; 84 | var list = Properties.Values.Where(p => !p.ShouldInclude && p.ComputedExpression != null).Select(p => p.ComputedExpression).ToList(); 85 | 86 | if (hasncludeFilter && list.Any()) 87 | { 88 | var w = MethodCreator.CreateGenericEnumerableWhereMethod(Type); 89 | var whereExpr = list.Aggregate(Expression.AndAlso); 90 | var l = Expression.Lambda(whereExpr, false, ParameterExpression); 91 | var c = Expression.Property(Parent.ParameterExpression, NodeName); 92 | includePropertyExpr = Expression.Call(null, w, new Expression[] { c, l }); 93 | include = Depth > 1 ? MethodCreator.CreateGenericThenIncludeMethod(Parent, this, typeof(IEnumerable<>)) : MethodCreator.CreateGenericIncludeMethod(Parent, this, typeof(IEnumerable<>)); 94 | } 95 | else 96 | { 97 | includePropertyExpr = Expression.Property(Parent.ParameterExpression, NodeName); 98 | include = Depth > 1 ? MethodCreator.CreateGenericThenIncludeMethod(Parent, this, typeof(ICollection<>)) : MethodCreator.CreateGenericIncludeMethod(Parent, this, typeof(ICollection<>)); 99 | } 100 | 101 | var lambda = Expression.Lambda(includePropertyExpr, false, Parent.ParameterExpression); 102 | var exp = Expression.Call(null, include, new[] { pExpression, Expression.Quote(lambda) }); 103 | return exp; 104 | } 105 | 106 | private Expression CreateAnyExpression() 107 | { 108 | if (WherePredictExpression == null) 109 | return null; 110 | 111 | var wInfo = MethodCreator.CreateGenericAnyMethod(Type); 112 | var lambda = Expression.Lambda(WherePredictExpression, false, ParameterExpression); 113 | var c = Expression.Property(Parent.ParameterExpression, NodeName); 114 | var exp = Expression.Call(null, wInfo, new Expression[] {c, Expression.Quote(lambda) }); 115 | 116 | return exp; 117 | } 118 | } 119 | } -------------------------------------------------------------------------------- /JsonQ/ExpressionOperator.cs: -------------------------------------------------------------------------------- 1 | namespace JsonQ 2 | { 3 | public enum ExpressionOperator 4 | { 5 | 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /JsonQ/Extensions/DbContextExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Microsoft.EntityFrameworkCore; 5 | using Newtonsoft.Json.Linq; 6 | 7 | namespace JsonQ.Extensions 8 | { 9 | public static class DbContextExtensions 10 | { 11 | public static IQueryable CreateQuery(this DbContext context, string queryString, List types) 12 | { 13 | return new JsonQueryableParser(queryString, types).ParseJsonString().CreateQuery(context); 14 | } 15 | 16 | public static IQueryable CreateQuery(this DbContext context, JObject jObject, List types) 17 | { 18 | return new JsonQueryableParser(jObject, types).ParseJsonString().CreateQuery(context); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /JsonQ/Extensions/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace JsonQ.Extensions 4 | { 5 | internal static class StringExtensions 6 | { 7 | /// 8 | /// If value is 'True' or '1' return true, otherwise return false. 9 | /// 10 | /// 11 | /// 12 | internal static bool ToBoolean(this string text) 13 | { 14 | if (text == null) return false; 15 | if (text.Trim() == "1") return true; 16 | 17 | bool result; 18 | bool.TryParse(text, out result); 19 | 20 | return result; 21 | } 22 | 23 | internal static double ToDouble(this string text) 24 | { 25 | double result; 26 | Double.TryParse(text, out result); 27 | 28 | return result; 29 | } 30 | 31 | internal static int ToInt32(this string text) 32 | { 33 | int result; 34 | Int32.TryParse(text, out result); 35 | 36 | return result; 37 | } 38 | 39 | internal static float ToFloat(this string text) 40 | { 41 | float result; 42 | float.TryParse(text, out result); 43 | 44 | return result; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /JsonQ/ILexer.cs: -------------------------------------------------------------------------------- 1 | namespace JsonQ 2 | { 3 | public interface ILexer 4 | { 5 | Token Lex(ISource source); 6 | 7 | Token Lex(ISource source, int start); 8 | } 9 | } -------------------------------------------------------------------------------- /JsonQ/ISource.cs: -------------------------------------------------------------------------------- 1 | namespace JsonQ 2 | { 3 | public interface ISource 4 | { 5 | string Body { get; set; } 6 | string Name { get; set; } 7 | } 8 | } -------------------------------------------------------------------------------- /JsonQ/JsonQ.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.1 5 | JsonQ 6 | JsonQ 7 | false 8 | false 9 | false 10 | true 11 | MaxMeng 12 | 1.3.0 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /JsonQ/JsonQueryableParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Linq.Expressions; 6 | using JsonQ.Extensions; 7 | using Newtonsoft.Json.Linq; 8 | 9 | namespace JsonQ 10 | { 11 | public class JsonQueryableParser 12 | { 13 | private readonly JObject _jObject; 14 | private readonly List _includedTypes; 15 | private readonly List _excludedTypes; 16 | internal QueryableCreator QueryableObject { get; private set; } 17 | 18 | public JsonQueryableParser(JObject jObject, List includedTypes) 19 | { 20 | _jObject = jObject; 21 | _includedTypes = includedTypes; 22 | } 23 | 24 | public JsonQueryableParser(string query, List includedTypes) 25 | { 26 | _jObject = JObject.Parse(query); 27 | _includedTypes = includedTypes; 28 | } 29 | 30 | internal QueryableCreator ParseJsonString() 31 | { 32 | var obj = new QueryableCreator(); 33 | Type rootType = null; 34 | JProperty rootJProperty = null; 35 | 36 | foreach (var property in this._jObject.Properties()) 37 | { 38 | var name = property.Name.ToLower(); 39 | switch (name) 40 | { 41 | case "type": 42 | { 43 | rootType = _includedTypes.First(t=> t.FullName != null && t.FullName.ToLower().Contains(property.Value.ToObject()?.ToLower() ?? "")); 44 | break; 45 | } 46 | case "query": 47 | { 48 | rootJProperty = property; 49 | break; 50 | } 51 | case "page": 52 | { 53 | if (property.Value.Type == JTokenType.Integer) 54 | { 55 | obj.Page = property.Value.ToObject(); 56 | } 57 | else 58 | { 59 | throw new Exception("Invalid page value"); 60 | } 61 | break; 62 | } 63 | case "pagesize": 64 | { 65 | if (property.Value.Type == JTokenType.Integer) 66 | { 67 | obj.PageSize = property.Value.ToObject(); 68 | } 69 | else 70 | { 71 | throw new Exception("Invalid pageSize value"); 72 | } 73 | break; 74 | } 75 | case "include": 76 | break; 77 | default: 78 | throw new NotSupportedException(); 79 | } 80 | } 81 | 82 | if (rootType == null) 83 | { 84 | throw new Exception("Unable to find property name 'type'."); 85 | } 86 | 87 | if (rootJProperty == null) 88 | { 89 | throw new Exception("Unable to find property name 'query'."); 90 | } 91 | 92 | obj.Root = ParseNode((JObject)rootJProperty.Value, rootType, rootJProperty.Name, false, true); 93 | 94 | var include = _jObject.Property("include"); 95 | 96 | if (include != null) 97 | { 98 | obj.Root.Include = ParseIncludeNodes(include.Value as JObject, rootType, obj.Root); 99 | } 100 | 101 | this.QueryableObject = obj; 102 | 103 | return obj; 104 | } 105 | 106 | private IList ParseIncludeNodes(JObject jObject, Type parentType, ExpressionNode parent) 107 | { 108 | var properties = jObject.Properties(); 109 | var list = new List(); 110 | 111 | foreach (var property in properties) 112 | { 113 | var obj = (JObject) property.Value; 114 | var query = obj.Property("query"); 115 | var include = obj.Property("include"); 116 | var type = parentType.GetProperty(property.Name)?.PropertyType; 117 | var isEnumerable = typeof(IEnumerable).IsAssignableFrom(type) && type?.GenericTypeArguments.Length > 0; 118 | var childeType = isEnumerable ? type.GenericTypeArguments[0] : type; 119 | var node = query == null 120 | ? new ExpressionNode(childeType, property.Name, true, isEnumerable, parent) 121 | : ParseNode(query.Value as JObject, childeType, property.Name, true, isEnumerable, parent); 122 | 123 | if (include != null) 124 | { 125 | node.Include = ParseIncludeNodes(include.Value as JObject, type, node); 126 | } 127 | 128 | list.Add(node); 129 | } 130 | 131 | return list; 132 | } 133 | 134 | private ExpressionNode ParseNode(JObject jObject, Type type, string name, bool isEnumerable, bool include, ExpressionNode parent = null) 135 | { 136 | var node = new ExpressionNode(type, name, include, isEnumerable, parent); 137 | 138 | foreach (var property in jObject.Properties()) 139 | { 140 | ExpressionNode childNode; 141 | 142 | var propertyType = node.Type.GetProperty(property.Name)?.PropertyType; 143 | //check is enumerable 144 | var childNodeisEnumerable = typeof(IEnumerable).IsAssignableFrom(propertyType) && propertyType?.GenericTypeArguments.Length > 0; 145 | //base type 146 | var childeType = childNodeisEnumerable ? propertyType.GenericTypeArguments[0] : propertyType; 147 | 148 | switch (property.Value.Type) 149 | { 150 | case JTokenType.Object when _includedTypes.Contains(childeType): 151 | childNode = ParseNode((JObject)property.Value, childeType, property.Name, childNodeisEnumerable, true, node); 152 | break; 153 | case JTokenType.Object: 154 | case JTokenType.Comment: 155 | continue; 156 | default: 157 | var expression = CreateLambdaExpression(node.ParameterExpression, property.Name, property.Value.ToString()); 158 | childNode = new ExpressionNode(childeType, property.Name, false, false, node, expression); 159 | break; 160 | } 161 | 162 | node.Properties.Add(property.Name, childNode); 163 | } 164 | 165 | return node; 166 | } 167 | 168 | private static Expression CreateLambdaExpression(ParameterExpression parameter, string property, string queryString) 169 | { 170 | Expression expression = null; 171 | var parse = Parser.Parse(new Source(queryString)); 172 | 173 | 174 | while (true) 175 | { 176 | Expression temp; 177 | var operation = parse.Next(); 178 | 179 | if (operation.Kind == TokenKind.EOF || operation.Kind == TokenKind.COMMA || operation.Kind == TokenKind.BRACE_L || operation.Kind == TokenKind.BRACE_R) 180 | { 181 | break; 182 | } 183 | 184 | TokenKind? kind = null; 185 | 186 | //if && || 187 | if (operation.Kind == TokenKind.AND || operation.Kind == TokenKind.OR) 188 | { 189 | kind = operation.Kind; 190 | operation = parse.Next(); 191 | } 192 | 193 | switch (operation.Kind) 194 | { 195 | case TokenKind.LESSTHAN: 196 | { 197 | var compare = LambdaCompare.LessThan; 198 | var n = parse.Next(); 199 | if (n.Kind == TokenKind.EQUALS) 200 | { 201 | n = parse.Next(); 202 | compare = LambdaCompare.LessThanOrEqual; 203 | } 204 | 205 | if (n.Kind == TokenKind.STRING && DateTime.TryParse(n.Value, out var dateTime)) 206 | { 207 | temp = CreateLambdaExpression(parameter, property, dateTime, compare); 208 | } 209 | else 210 | { 211 | var v = n.Value.ToInt32(); 212 | temp = CreateLambdaExpression(parameter, property, v, compare); 213 | } 214 | } 215 | break; 216 | case TokenKind.GREATERTHAN: 217 | { 218 | var compare = LambdaCompare.GreaterThan; 219 | var n = parse.Next(); 220 | if (n.Kind == TokenKind.EQUALS) 221 | { 222 | n = parse.Next(); 223 | compare = LambdaCompare.GreaterThanOrEqual; 224 | } 225 | 226 | if (n.Kind == TokenKind.STRING && DateTime.TryParse(n.Value, out var dateTime)) 227 | { 228 | temp = CreateLambdaExpression(parameter, property, dateTime, compare); 229 | } 230 | else 231 | { 232 | var v = n.Value.ToInt32(); 233 | temp = CreateLambdaExpression(parameter, property, v, compare); 234 | } 235 | 236 | break; 237 | } 238 | case TokenKind.EQUALS: 239 | { 240 | var n = parse.Next(); 241 | 242 | switch (n.Kind) 243 | { 244 | case TokenKind.STRING: 245 | { 246 | temp = DateTime.TryParse(n.Value, out var dateTime) 247 | ? CreateLambdaExpression(parameter, property, dateTime, LambdaCompare.Equal) 248 | : CreateLambdaExpression(parameter, property, n.Value, LambdaCompare.Equal); 249 | 250 | break; 251 | } 252 | case TokenKind.INT: 253 | { 254 | var v = n.Value.ToInt32(); 255 | temp = CreateLambdaExpression(parameter, property, v, LambdaCompare.Equal); 256 | break; 257 | } 258 | default: 259 | throw new IndexOutOfRangeException(); 260 | } 261 | 262 | break; 263 | } 264 | case TokenKind.INT: 265 | { 266 | var v = operation.Value.ToInt32(); 267 | temp = CreateLambdaExpression(parameter, property, v, LambdaCompare.Equal); 268 | } 269 | break; 270 | case TokenKind.STRING: 271 | { 272 | var v = operation.Value; 273 | temp = CreateLambdaExpression(parameter, property, v, LambdaCompare.Equal); 274 | } 275 | break; 276 | case TokenKind.BOOLEAN: 277 | { 278 | var v = operation.Value.ToBoolean(); 279 | temp = CreateLambdaExpression(parameter, property, v, LambdaCompare.Equal); 280 | } 281 | break; 282 | case TokenKind.NAME: 283 | { 284 | var operationName = operation.Value.ToLower(); 285 | switch (operationName) 286 | { 287 | case "$contains": 288 | operation = parse.Next(); 289 | var v = operation.Value; 290 | temp = CreateLambdaExpression(parameter, property, v, LambdaCompare.Contains); 291 | break; 292 | default: 293 | throw new NotSupportedException(operationName); 294 | } 295 | break; 296 | } 297 | case TokenKind.DOLLAR: 298 | { 299 | var operationName = parse.Next().Value.ToLower(); 300 | switch (operationName) 301 | { 302 | case "contains": 303 | operation = parse.Next(); 304 | var v = operation.Value; 305 | temp = CreateLambdaExpression(parameter, property, v, LambdaCompare.Contains); 306 | break; 307 | default: 308 | throw new NotSupportedException(operationName); 309 | } 310 | break; 311 | } 312 | default: 313 | throw new NotImplementedException(); 314 | } 315 | 316 | if (kind != null && expression != null) 317 | { 318 | switch (kind) 319 | { 320 | case TokenKind.AND: 321 | temp = Expression.AndAlso(expression, temp); 322 | break; 323 | case TokenKind.OR: 324 | temp = Expression.OrElse(expression, temp); 325 | break; 326 | default: 327 | throw new Exception(kind.ToString()); 328 | } 329 | } 330 | 331 | expression = temp; 332 | } 333 | 334 | return expression; 335 | } 336 | 337 | private static Expression CreateLambdaExpression(ParameterExpression parameter, string property, object value, LambdaCompare compare) 338 | { 339 | //var propertyExpression = Expression.Convert(Expression.Property(parameter, property), value.GetType()); 340 | var propertyExpression = Expression.Property(parameter, property); 341 | var valueExpression = Expression.Constant(value); 342 | 343 | switch (compare) 344 | { 345 | case LambdaCompare.LessThan: 346 | return Expression.LessThan(propertyExpression, valueExpression); 347 | case LambdaCompare.LessThanOrEqual: 348 | return Expression.LessThanOrEqual(propertyExpression, valueExpression); 349 | case LambdaCompare.GreaterThan: 350 | return Expression.GreaterThan(propertyExpression, valueExpression); 351 | case LambdaCompare.GreaterThanOrEqual: 352 | return Expression.GreaterThanOrEqual(propertyExpression, valueExpression); 353 | case LambdaCompare.Equal: 354 | return Expression.Equal(propertyExpression, valueExpression); 355 | case LambdaCompare.NotEqual: 356 | return Expression.NotEqual(propertyExpression, valueExpression); 357 | case LambdaCompare.Contains: 358 | return Expression.Call(propertyExpression, MethodCreator.CreateStringContainMethod(), valueExpression); 359 | case LambdaCompare.Unknown: 360 | default: 361 | throw new ArgumentOutOfRangeException(nameof(compare), compare, null); 362 | } 363 | } 364 | } 365 | } 366 | -------------------------------------------------------------------------------- /JsonQ/LambdaCompare.cs: -------------------------------------------------------------------------------- 1 | namespace JsonQ 2 | { 3 | public enum LambdaCompare 4 | { 5 | Unknown = 0, 6 | LessThan = 1, 7 | LessThanOrEqual = 2, 8 | GreaterThan = 3, 9 | GreaterThanOrEqual = 4, 10 | Equal = 5, 11 | NotEqual = 6, 12 | Contains = 7 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /JsonQ/Lexer.cs: -------------------------------------------------------------------------------- 1 | namespace JsonQ 2 | { 3 | public class Lexer : ILexer 4 | { 5 | public Token Lex(ISource source) 6 | { 7 | return this.Lex(source, 0); 8 | } 9 | 10 | public Token Lex(ISource source, int start) 11 | { 12 | using var context = new LexerContext(source, start); 13 | return context.GetToken(); 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /JsonQ/LexerContext.cs: -------------------------------------------------------------------------------- 1 | namespace JsonQ 2 | { 3 | using Exceptions; 4 | using System; 5 | 6 | public class LexerContext : IDisposable 7 | { 8 | private int _currentIndex; 9 | private readonly ISource _source; 10 | 11 | public LexerContext(ISource source, int index) 12 | { 13 | _currentIndex = index; 14 | _source = source; 15 | } 16 | 17 | public void Dispose() 18 | { 19 | } 20 | 21 | public Token GetToken() 22 | { 23 | if (_source.Body == null) 24 | return CreateEOFToken(); 25 | 26 | _currentIndex = GetPositionAfterWhitespace(_source.Body, _currentIndex); 27 | 28 | if (_currentIndex >= _source.Body.Length) 29 | return CreateEOFToken(); 30 | 31 | var unicode = IfUnicodeGetString(); 32 | 33 | var code = _source.Body[_currentIndex]; 34 | 35 | ValidateCharacterCode(code); 36 | 37 | var token = CheckForPunctuationTokens(code); 38 | if (token != null) 39 | return token; 40 | 41 | if (char.IsLetter(code) || code == '_') 42 | return ReadNameOrBoolean(); 43 | 44 | if (char.IsNumber(code) || code == '-') 45 | return ReadNumber(); 46 | 47 | if (code == '"' || code == '\'') 48 | return ReadString(); 49 | 50 | throw new QuerySyntaxErrorException( 51 | $"Unexpected character {ResolveCharName(code, unicode)}", _source, _currentIndex); 52 | } 53 | 54 | public bool OnlyHexInString(string test) 55 | { 56 | return System.Text.RegularExpressions.Regex.IsMatch(test, @"\A\b[0-9a-fA-F]+\b\Z"); 57 | } 58 | 59 | public Token ReadNumber() 60 | { 61 | var isFloat = false; 62 | var start = _currentIndex; 63 | var code = _source.Body[start]; 64 | 65 | if (code == '-') 66 | code = NextCode(); 67 | 68 | var nextCode = code == '0' 69 | ? NextCode() 70 | : ReadDigitsFromOwnSource(code); 71 | 72 | if (nextCode >= 48 && nextCode <= 57) 73 | { 74 | throw new QuerySyntaxErrorException( 75 | $"Invalid number, unexpected digit after {code}: \"{nextCode}\"", _source, _currentIndex); 76 | } 77 | 78 | code = nextCode; 79 | if (code == '.') 80 | { 81 | isFloat = true; 82 | code = ReadDigitsFromOwnSource(NextCode()); 83 | } 84 | 85 | if (code == 'E' || code == 'e') 86 | { 87 | isFloat = true; 88 | code = NextCode(); 89 | if (code == '+' || code == '-') 90 | { 91 | NextCode(); 92 | } 93 | } 94 | 95 | return isFloat ? CreateFloatToken(start) : CreateIntToken(start); 96 | } 97 | 98 | public Token ReadString() 99 | { 100 | var start = _currentIndex; 101 | var value = ProcessStringChunks(); 102 | 103 | return new Token() 104 | { 105 | Kind = TokenKind.STRING, 106 | Value = value, 107 | Start = start, 108 | End = _currentIndex + 1 109 | }; 110 | } 111 | 112 | private static bool IsValidNameCharacter(char code) 113 | { 114 | return code == '_' || char.IsLetterOrDigit(code); 115 | } 116 | 117 | private string AppendCharactersFromLastChunk(string value, int chunkStart) 118 | { 119 | return value + _source.Body.Substring(chunkStart, _currentIndex - chunkStart - 1); 120 | } 121 | 122 | private string AppendToValueByCode(string value, char code) 123 | { 124 | switch (code) 125 | { 126 | case '"': value += '"'; break; 127 | case '/': value += '/'; break; 128 | case '\\': value += '\\'; break; 129 | case 'b': value += '\b'; break; 130 | case 'f': value += '\f'; break; 131 | case 'n': value += '\n'; break; 132 | case 'r': value += '\r'; break; 133 | case 't': value += '\t'; break; 134 | case 'u': value += GetUnicodeChar(); break; 135 | default: 136 | throw new QuerySyntaxErrorException($"Invalid character escape sequence: \\{code}.", _source, _currentIndex); 137 | } 138 | 139 | return value; 140 | } 141 | 142 | private int CharToHex(char code) 143 | { 144 | return Convert.ToByte(code.ToString(), 16); 145 | } 146 | 147 | private void CheckForInvalidCharacters(char code) 148 | { 149 | if (code < 0x0020 && code != 0x0009) 150 | { 151 | throw new QuerySyntaxErrorException( 152 | $"Invalid character within String: \\u{((int)code):D4}.", _source, _currentIndex); 153 | } 154 | } 155 | 156 | private Token CheckForPunctuationTokens(char code) 157 | { 158 | switch (code) 159 | { 160 | case '$': return CreatePunctuationToken(TokenKind.DOLLAR, 1); 161 | case '(': return CreatePunctuationToken(TokenKind.PAREN_L, 1); 162 | case ')': return CreatePunctuationToken(TokenKind.PAREN_R, 1); 163 | case ':': return CreatePunctuationToken(TokenKind.COLON, 1); 164 | case '@': return CreatePunctuationToken(TokenKind.AT, 1); 165 | case '[': return CreatePunctuationToken(TokenKind.BRACKET_L, 1); 166 | case ']': return CreatePunctuationToken(TokenKind.BRACKET_R, 1); 167 | case '{': return CreatePunctuationToken(TokenKind.BRACE_L, 1); 168 | case '}': return CreatePunctuationToken(TokenKind.BRACE_R, 1); 169 | case ',': return CreatePunctuationToken(TokenKind.COMMA, 1); 170 | case '<': return CreatePunctuationToken(TokenKind.LESSTHAN, 1); 171 | case '>': return CreatePunctuationToken(TokenKind.GREATERTHAN, 1); 172 | case '.': return CheckForSpreadOperator(); 173 | case '&': return CheckForAnd(); 174 | case '|': return CheckForOr(); 175 | case '=': return CheckForEquals(); 176 | case '!': return CheckForNot(); 177 | default: return null; 178 | } 179 | } 180 | 181 | private Token CheckForAnd() 182 | { 183 | return CheckFor('&', 1, TokenKind.AND) ?? CreatePunctuationToken(TokenKind.AND, 1); 184 | } 185 | 186 | private Token CheckForOr() 187 | { 188 | return CheckFor('|', 1, TokenKind.OR) ?? CreatePunctuationToken(TokenKind.PIPE, 1); 189 | } 190 | 191 | private Token CheckForNot() 192 | { 193 | return CheckFor('=', 1, TokenKind.NOT) ?? CreatePunctuationToken(TokenKind.BANG, 1); 194 | } 195 | 196 | private Token CheckForEquals() 197 | { 198 | return CheckFor('=', 1, TokenKind.EQUALS) ?? CreatePunctuationToken(TokenKind.EQUALS, 1); 199 | } 200 | 201 | private Token CheckFor(char c, int length, TokenKind kind) 202 | { 203 | for (var i = 1; i <= length; i++) 204 | { 205 | var cc = _source.Body.Length > _currentIndex + i ? _source.Body[_currentIndex + i] : 0; 206 | 207 | if (cc != c) 208 | { 209 | return null; 210 | } 211 | } 212 | 213 | return CreatePunctuationToken(kind, length + 1); 214 | } 215 | 216 | private Token CheckForSpreadOperator() 217 | { 218 | return CheckFor('.', 2, TokenKind.SPREAD); 219 | } 220 | 221 | private void CheckStringTermination(char code) 222 | { 223 | if (code != '"' && code != '\'') 224 | { 225 | throw new QuerySyntaxErrorException("Unterminated string.", _source, _currentIndex); 226 | } 227 | } 228 | 229 | private Token CreateEOFToken() 230 | { 231 | return new Token() 232 | { 233 | Start = _currentIndex, 234 | End = _currentIndex, 235 | Kind = TokenKind.EOF 236 | }; 237 | } 238 | 239 | private Token CreateFloatToken(int start) 240 | { 241 | return new Token() 242 | { 243 | Kind = TokenKind.FLOAT, 244 | Start = start, 245 | End = _currentIndex, 246 | Value = _source.Body.Substring(start, _currentIndex - start) 247 | }; 248 | } 249 | 250 | private Token CreateIntToken(int start) 251 | { 252 | return new Token() 253 | { 254 | Kind = TokenKind.INT, 255 | Start = start, 256 | End = _currentIndex, 257 | Value = _source.Body.Substring(start, _currentIndex - start) 258 | }; 259 | } 260 | 261 | private Token CreateNameOrBooleanToken(int start) 262 | { 263 | var value = _source.Body.Substring(start, _currentIndex - start); 264 | var isBoolean = value.Equals("true", StringComparison.OrdinalIgnoreCase) || value.Equals("false", StringComparison.OrdinalIgnoreCase); 265 | 266 | return new Token() 267 | { 268 | Start = start, 269 | End = _currentIndex, 270 | Kind = isBoolean ? TokenKind.BOOLEAN : TokenKind.NAME, 271 | Value = value 272 | }; 273 | } 274 | 275 | private Token CreatePunctuationToken(TokenKind kind, int offset) 276 | { 277 | return new Token() 278 | { 279 | Start = _currentIndex, 280 | End = _currentIndex + offset, 281 | Kind = kind, 282 | Value = null 283 | }; 284 | } 285 | 286 | private char GetCode() 287 | { 288 | return IsNotAtTheEndOfQuery() 289 | ? _source.Body[_currentIndex] 290 | : (char)0; 291 | } 292 | 293 | private int GetPositionAfterWhitespace(string body, int start) 294 | { 295 | var position = start; 296 | 297 | while (position < body.Length) 298 | { 299 | var code = body[position]; 300 | switch (code) 301 | { 302 | case '\xFEFF': // BOM 303 | case '\t': // tab 304 | case ' ': // space 305 | case '\n': // new line 306 | case '\r': // carriage return 307 | //case ',': // Comma 308 | ++position; 309 | break; 310 | 311 | case '#': 312 | position = WaitForEndOfComment(body, position, code); 313 | break; 314 | 315 | default: 316 | return position; 317 | } 318 | } 319 | 320 | return position; 321 | } 322 | 323 | private char GetUnicodeChar() 324 | { 325 | var expression = _source.Body.Substring(_currentIndex, 5); 326 | 327 | if (!OnlyHexInString(expression.Substring(1))) 328 | { 329 | throw new QuerySyntaxErrorException($"Invalid character escape sequence: \\{expression}.", _source, _currentIndex); 330 | } 331 | 332 | var character = (char)( 333 | CharToHex(NextCode()) << 12 | 334 | CharToHex(NextCode()) << 8 | 335 | CharToHex(NextCode()) << 4 | 336 | CharToHex(NextCode())); 337 | 338 | return character; 339 | } 340 | 341 | private string IfUnicodeGetString() 342 | { 343 | return _source.Body.Length > _currentIndex + 5 && 344 | OnlyHexInString(_source.Body.Substring(_currentIndex + 2, 4)) 345 | ? _source.Body.Substring(_currentIndex, 6) 346 | : null; 347 | } 348 | 349 | private bool IsNotAtTheEndOfQuery() 350 | { 351 | return _currentIndex < _source.Body.Length; 352 | } 353 | 354 | private char NextCode() 355 | { 356 | _currentIndex++; 357 | return IsNotAtTheEndOfQuery() 358 | ? _source.Body[_currentIndex] 359 | : (char)0; 360 | } 361 | 362 | private char ProcessCharacter(ref string value, ref int chunkStart) 363 | { 364 | var code = GetCode(); 365 | ++_currentIndex; 366 | 367 | if (code == '\\') 368 | { 369 | value = AppendToValueByCode(AppendCharactersFromLastChunk(value, chunkStart), GetCode()); 370 | 371 | ++_currentIndex; 372 | chunkStart = _currentIndex; 373 | } 374 | 375 | return GetCode(); 376 | } 377 | 378 | private string ProcessStringChunks() 379 | { 380 | var chunkStart = ++_currentIndex; 381 | var code = GetCode(); 382 | var value = string.Empty; 383 | 384 | while (IsNotAtTheEndOfQuery() && code != 0x000A && code != 0x000D && code != '"' && code != '\'') 385 | { 386 | CheckForInvalidCharacters(code); 387 | code = ProcessCharacter(ref value, ref chunkStart); 388 | } 389 | 390 | CheckStringTermination(code); 391 | value += _source.Body.Substring(chunkStart, _currentIndex - chunkStart); 392 | return value; 393 | } 394 | 395 | private int ReadDigits(ISource source, int start, char firstCode) 396 | { 397 | var body = source.Body; 398 | var position = start; 399 | var code = firstCode; 400 | 401 | if (!char.IsNumber(code)) 402 | { 403 | throw new QuerySyntaxErrorException( 404 | $"Invalid number, expected digit but got: {ResolveCharName(code)}", _source, _currentIndex); 405 | } 406 | 407 | do 408 | { 409 | code = ++position < body.Length 410 | ? body[position] 411 | : (char)0; 412 | } 413 | while (char.IsNumber(code)); 414 | 415 | return position; 416 | } 417 | 418 | private char ReadDigitsFromOwnSource(char code) 419 | { 420 | _currentIndex = ReadDigits(_source, _currentIndex, code); 421 | code = GetCode(); 422 | return code; 423 | } 424 | 425 | private Token ReadNameOrBoolean() 426 | { 427 | var start = _currentIndex; 428 | var code = (char)0; 429 | 430 | do 431 | { 432 | _currentIndex++; 433 | code = GetCode(); 434 | } 435 | while (IsNotAtTheEndOfQuery() && IsValidNameCharacter(code)); 436 | 437 | return CreateNameOrBooleanToken(start); 438 | } 439 | 440 | private string ResolveCharName(char code, string unicodeString = null) 441 | { 442 | if (code == '\0') 443 | return ""; 444 | 445 | if (!string.IsNullOrWhiteSpace(unicodeString)) 446 | return $"\"{unicodeString}\""; 447 | 448 | return $"\"{code}\""; 449 | } 450 | 451 | private void ValidateCharacterCode(int code) 452 | { 453 | if (code < 0x0020 && code != 0x0009 && code != 0x000A && code != 0x000D) 454 | { 455 | throw new QuerySyntaxErrorException( 456 | $"Invalid character \"\\u{code:D4}\".", _source, _currentIndex); 457 | } 458 | } 459 | 460 | private int WaitForEndOfComment(string body, int position, char code) 461 | { 462 | while (++position < body.Length && (code = body[position]) != 0 && (code > 0x001F || code == 0x0009) && code != 0x000A && code != 0x000D) 463 | { 464 | } 465 | 466 | return position; 467 | } 468 | } 469 | } -------------------------------------------------------------------------------- /JsonQ/Location.cs: -------------------------------------------------------------------------------- 1 | namespace JsonQ 2 | { 3 | using System.Text.RegularExpressions; 4 | 5 | public class Location 6 | { 7 | public Location(ISource source, int position) 8 | { 9 | var lineRegex = new Regex("\r\n|[\n\r]", RegexOptions.ECMAScript); 10 | this.Line = 1; 11 | this.Column = position + 1; 12 | 13 | var matches = lineRegex.Matches(source.Body); 14 | foreach (Match match in matches) 15 | { 16 | if (match.Index >= position) 17 | break; 18 | 19 | this.Line++; 20 | this.Column = position + 1 - (match.Index + matches[0].Length); 21 | } 22 | } 23 | 24 | public int Column { get; private set; } 25 | public int Line { get; private set; } 26 | } 27 | } -------------------------------------------------------------------------------- /JsonQ/MethodCreator.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Reflection; 6 | 7 | namespace JsonQ 8 | { 9 | internal static class MethodCreator 10 | { 11 | internal const string ANY = "Any"; 12 | internal const string WHERE = "Where"; 13 | internal const string SELECT = "Select"; 14 | internal const string CONTAINS = "Contains"; 15 | internal const string INCLUDE = "Include"; 16 | internal const string THEN_INCLUDE = "ThenInclude"; 17 | internal const string INCLUDE_WITH_FILTER = "IncludeWithFilter"; 18 | internal const string THEN_INCLUDE_WITH_FILTER = "ThenIncludeWithFilter"; 19 | 20 | internal static MethodInfo CreateGenericIncludeMethod(ExpressionNode parent, ExpressionNode node, Type collectionType) 21 | { 22 | var propertyType = node.IsEnumerable ? collectionType.MakeGenericType(node.Type) : node.Type; 23 | var mInfo = typeof(EntityFrameworkQueryableExtensions).GetMethods() 24 | .Where(m => m.Name == INCLUDE) 25 | .First(m => m.GetGenericArguments().Length == 2) 26 | .MakeGenericMethod(parent.Type, propertyType); 27 | return mInfo; 28 | } 29 | 30 | internal static MethodInfo CreateGenericThenIncludeMethod(ExpressionNode parent, ExpressionNode node, Type collectionType) 31 | { 32 | var propertyType = node.IsEnumerable ? collectionType.MakeGenericType(node.Type) : node.Type; 33 | var mInfo = typeof(EntityFrameworkQueryableExtensions).GetMethods() 34 | .Where(m => m.Name == THEN_INCLUDE).ElementAt(parent.IsEnumerable ? 0 : 1) 35 | .MakeGenericMethod(parent.Root.Type, parent.Type, propertyType); 36 | return mInfo; 37 | } 38 | 39 | internal static MethodInfo CreateGenericIncludeWithFilterMethod(ExpressionNode parent, ExpressionNode node) 40 | { 41 | var propertyType = node.IsEnumerable ? typeof(ICollection<>).MakeGenericType(node.Type) : node.Type; 42 | var mInfo = Type.GetType("EntityFrameworkCore.IncludeFilter.QueryableExtensions,EntityFrameworkCore.IncludeFilter")?.GetMethods() 43 | .Where(m => m.Name == INCLUDE_WITH_FILTER) 44 | .First(m => m.GetGenericArguments().Length == 2) 45 | .MakeGenericMethod(parent.Type, propertyType); 46 | return mInfo; 47 | } 48 | 49 | internal static MethodInfo CreateGenericThenIncludeWithFilterMethod(ExpressionNode parent, ExpressionNode node) 50 | { 51 | var propertyType = node.IsEnumerable ? typeof(ICollection<>).MakeGenericType(node.Type) : node.Type; 52 | var mInfo = Type.GetType("EntityFrameworkCore.IncludeFilter.QueryableExtensions,EntityFrameworkCore.IncludeFilter")?.GetMethods() 53 | .Where(m => m.Name == THEN_INCLUDE_WITH_FILTER) 54 | .First(m => m.GetGenericArguments().Length == 3) 55 | .MakeGenericMethod(parent.Root.Type, parent.Type, propertyType); 56 | return mInfo; 57 | } 58 | 59 | internal static MethodInfo CreateGenericQueryableWhereMethod(Type type) 60 | { 61 | var qInfo = typeof(Queryable); 62 | var mInfos = qInfo.GetMethods().Where(m => m.Name == WHERE); 63 | var mInfo = mInfos.First(m => m.GetGenericArguments().Length == 1).MakeGenericMethod(type); 64 | return mInfo; 65 | } 66 | 67 | internal static MethodInfo CreateStringContainMethod() 68 | { 69 | var qInfo = typeof(string); 70 | var mInfos = qInfo.GetMethods().Where(m => m.Name == CONTAINS); 71 | var mInfo = mInfos.First(); 72 | return mInfo; 73 | } 74 | 75 | internal static MethodInfo CreateGenericSelectMethod(Type type) 76 | { 77 | var qInfo = typeof(Queryable); 78 | var mInfos = qInfo.GetMethods().Where(m => m.Name == SELECT); 79 | var mInfo = mInfos.First(m => m.GetGenericArguments().Length == 2).MakeGenericMethod(type, type); 80 | return mInfo; 81 | } 82 | 83 | 84 | internal static MethodInfo CreateGenericAnyMethod(Type type) 85 | { 86 | var qInfo = typeof(Enumerable); 87 | var mInfos = qInfo.GetMethods().Where(m => m.Name == MethodCreator.ANY); 88 | var mInfo = mInfos.First(m => m.GetGenericArguments().Length == 1 && m.GetParameters().Length == 2).MakeGenericMethod(type); 89 | return mInfo; 90 | } 91 | 92 | internal static MethodInfo CreateGenericEnumerableWhereMethod(Type type) 93 | { 94 | var qInfo = typeof(Enumerable); 95 | var mInfos = qInfo.GetMethods().Where(m => m.Name == MethodCreator.WHERE); 96 | var mInfo = mInfos.First(m => m.GetGenericArguments().Length == 1 && m.GetParameters().Length == 2).MakeGenericMethod(type); 97 | return mInfo; 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /JsonQ/Parser.cs: -------------------------------------------------------------------------------- 1 | namespace JsonQ 2 | { 3 | public class Parser 4 | { 5 | private readonly ILexer _lexer; 6 | private ParserContext _parserContext; 7 | 8 | private Parser(ILexer lexer) 9 | { 10 | this._lexer = lexer; 11 | } 12 | 13 | public static Parser Parse(ISource source) 14 | { 15 | var parser = new Parser(new Lexer()); 16 | parser._parserContext = new ParserContext(source, parser._lexer); 17 | return parser; 18 | } 19 | 20 | public Token Next() 21 | { 22 | return this._parserContext.Next(); 23 | } 24 | 25 | public Token Back() 26 | { 27 | return this._parserContext.Back(); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /JsonQ/ParserContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace JsonQ 4 | { 5 | public class ParserContext : IDisposable 6 | { 7 | private Token _currentToken; 8 | private readonly ILexer _lexer; 9 | private readonly ISource _source; 10 | 11 | public ParserContext(ISource source, ILexer lexer) 12 | { 13 | this._source = source; 14 | this._lexer = lexer; 15 | 16 | this._currentToken = null; 17 | } 18 | 19 | public Token Next() 20 | { 21 | if (_currentToken?.Next != null) 22 | { 23 | _currentToken = _currentToken.Next; 24 | } 25 | else 26 | { 27 | var t = _currentToken; 28 | _currentToken = this._lexer.Lex(_source, _currentToken?.End ?? 0); 29 | _currentToken.Previous = t; 30 | 31 | if(t != null) 32 | t.Next = _currentToken; 33 | } 34 | 35 | return _currentToken; 36 | } 37 | 38 | public void Dispose() 39 | { 40 | } 41 | 42 | public Token Back() 43 | { 44 | this._currentToken = _currentToken?.Previous ?? throw new Exception("_currentToken.PreviousToken == null"); 45 | return _currentToken; 46 | } 47 | 48 | public void Advance() 49 | { 50 | this._currentToken = this._lexer.Lex(this._source, this._currentToken.End); 51 | } 52 | 53 | public Token CurrentToken => _currentToken; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /JsonQ/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyConfiguration("")] 9 | [assembly: AssemblyCompany("")] 10 | [assembly: AssemblyProduct("JsonQ")] 11 | [assembly: AssemblyTrademark("")] 12 | 13 | // Setting ComVisible to false makes the types in this assembly not visible 14 | // to COM components. If you need to access a type in this assembly from 15 | // COM, set the ComVisible attribute to true on that type. 16 | [assembly: ComVisible(false)] 17 | 18 | // The following GUID is for the ID of the typelib if this project is exposed to COM 19 | [assembly: Guid("7dba4f4d-02c3-43c2-9c49-f2ddfc003734")] 20 | -------------------------------------------------------------------------------- /JsonQ/QueryableCreator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Linq.Expressions; 4 | using System.Reflection; 5 | using Microsoft.EntityFrameworkCore; 6 | 7 | namespace JsonQ 8 | { 9 | public class QueryableCreator 10 | { 11 | public ExpressionNode Root { get; set; } 12 | public int Page { get; set; } = 0; 13 | public int PageSize { get; set; } = 0; 14 | 15 | private static readonly MethodInfo CreateQueryMethodInfo = typeof(QueryableCreator).GetMethod("CreateQuery", new [] {typeof(DbContext)}); 16 | 17 | public IQueryable CreateQuery(DbContext context, Type type = null) 18 | { 19 | return CreateQueryMethodInfo.MakeGenericMethod(type ?? this.Root.Type).Invoke(this, new object[]{ context }) as IQueryable; 20 | } 21 | 22 | public IQueryable CreateQuery(DbContext context) where T : class 23 | { 24 | const bool hasIncludeFilter = true; 25 | 26 | var set = context.Set(); 27 | var q = CreateQueryInclude(set, Root, hasIncludeFilter); 28 | q = CreateQueryWhere(q, Root); 29 | 30 | if (PageSize > 0) 31 | { 32 | q = q.Skip(Page * PageSize).Take(PageSize); 33 | } 34 | 35 | //if (n.Properties.Count <= 0) 36 | // return q; 37 | 38 | //q = CreateQuerySelect(q, _rootType, n); 39 | 40 | return q; 41 | } 42 | 43 | private static IQueryable CreateQueryInclude(IQueryable queryable, ExpressionNode expressionNode, bool hasIncludeFilter) 44 | { 45 | if (expressionNode.Include == null || expressionNode.Include.Count == 0) 46 | return queryable; 47 | 48 | var temp = queryable; 49 | 50 | foreach (var node in expressionNode.Include) 51 | { 52 | var exp = node.CreateIncludeExpression(temp.Expression, hasIncludeFilter); 53 | temp = queryable.Provider.CreateQuery(exp); 54 | } 55 | 56 | return temp; 57 | } 58 | 59 | private static IQueryable CreateQueryWhere(IQueryable queryable, ExpressionNode node) 60 | { 61 | var predict = node.CreateWherePredictExpression(); 62 | 63 | if (predict == null) 64 | return queryable; 65 | 66 | var wInfo = MethodCreator.CreateGenericQueryableWhereMethod(node.Type); 67 | var lambda = Expression.Lambda(node.WherePredictExpression, false, node.ParameterExpression); 68 | var exp = Expression.Call(null, wInfo, new[] { queryable.Expression, Expression.Quote(lambda) }); 69 | 70 | return queryable.Provider.CreateQuery(exp); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /JsonQ/Source.cs: -------------------------------------------------------------------------------- 1 | namespace JsonQ 2 | { 3 | public class Source : ISource 4 | { 5 | public Source(string body, string name = "Query") 6 | { 7 | this.Name = name; 8 | this.Body = MonetizeLineBreaks(body); 9 | } 10 | 11 | public string Body { get; set; } 12 | public string Name { get; set; } 13 | 14 | private static string MonetizeLineBreaks(string input) 15 | { 16 | return (input ?? string.Empty) 17 | .Replace("\r\n", "\n") 18 | .Replace("\r", "\n"); 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /JsonQ/Token.cs: -------------------------------------------------------------------------------- 1 | namespace JsonQ 2 | { 3 | public enum TokenKind 4 | { 5 | EOF = 1, 6 | BANG = 2, 7 | DOLLAR = 3, 8 | PAREN_L = 4, 9 | PAREN_R = 5, 10 | SPREAD = 6, 11 | COLON = 7, 12 | EQUALS = 8, 13 | AT = 9, 14 | BRACKET_L = 10, 15 | BRACKET_R = 11, 16 | BRACE_L = 12, 17 | PIPE = 13, 18 | BRACE_R = 14, 19 | NAME = 15, 20 | INT = 16, 21 | FLOAT = 17, 22 | STRING = 18, 23 | COMMA = 19, 24 | LESSTHAN = 20, 25 | GREATERTHAN = 21, 26 | AND = 22, 27 | OR = 23, 28 | NOT = 24, 29 | BOOLEAN = 25, 30 | SINGLUE_QUOTE = 26, 31 | } 32 | 33 | public class Token 34 | { 35 | public int End { get; set; } 36 | public TokenKind Kind { get; set; } 37 | public int Start { get; set; } 38 | public string Value { get; set; } 39 | 40 | public static string GetTokenKindDescription(TokenKind kind) 41 | { 42 | switch (kind) 43 | { 44 | case TokenKind.EOF: return "EOF"; 45 | case TokenKind.BANG: return "!"; 46 | case TokenKind.DOLLAR: return "$"; 47 | case TokenKind.PAREN_L: return "("; 48 | case TokenKind.PAREN_R: return ")"; 49 | case TokenKind.SPREAD: return "..."; 50 | case TokenKind.COLON: return ":"; 51 | case TokenKind.EQUALS: return "="; 52 | case TokenKind.AT: return "@"; 53 | case TokenKind.BRACKET_L: return "["; 54 | case TokenKind.BRACKET_R: return "]"; 55 | case TokenKind.BRACE_L: return "{"; 56 | case TokenKind.PIPE: return "|"; 57 | case TokenKind.BRACE_R: return "}"; 58 | case TokenKind.NAME: return "Name"; 59 | case TokenKind.INT: return "Int"; 60 | case TokenKind.FLOAT: return "Float"; 61 | case TokenKind.STRING: return "String"; 62 | case TokenKind.COMMA: return ","; 63 | case TokenKind.LESSTHAN: return "<"; 64 | case TokenKind.GREATERTHAN: return ">"; 65 | } 66 | 67 | return string.Empty; 68 | } 69 | 70 | public Token Previous { get; set; } 71 | public Token Next { get; set; } 72 | 73 | public override string ToString() 74 | { 75 | return this.Value != null 76 | ? $"{GetTokenKindDescription(this.Kind)} \"{this.Value}\"" 77 | : GetTokenKindDescription(this.Kind); 78 | } 79 | } 80 | } -------------------------------------------------------------------------------- /JsonToQueryable.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.28010.2046 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonQ", "JsonQ\JsonQ.csproj", "{131878EB-700B-42BF-8638-4721D1CE8329}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {131878EB-700B-42BF-8638-4721D1CE8329}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {131878EB-700B-42BF-8638-4721D1CE8329}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {131878EB-700B-42BF-8638-4721D1CE8329}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {131878EB-700B-42BF-8638-4721D1CE8329}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {450831AC-DDD6-4183-A409-6B9828F6C2AA} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017, Qinglin (Max) Meng 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | 23 | Parser modified based on Marek's project 24 | 25 | The MIT License (MIT) 26 | 27 | Copyright (c) 2016 Marek Magdziak 28 | 29 | Permission is hereby granted, free of charge, to any person obtaining a copy 30 | of this software and associated documentation files (the "Software"), to deal 31 | in the Software without restriction, including without limitation the rights 32 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 33 | copies of the Software, and to permit persons to whom the Software is 34 | furnished to do so, subject to the following conditions: 35 | 36 | The above copyright notice and this permission notice shall be included in all 37 | copies or substantial portions of the Software. 38 | 39 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 40 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 41 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 42 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 43 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 44 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 45 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JsonToQueryable 2 | 3 | Parse JSON string to expressions that used for querying in EntityFramework.Core 5.0 4 | 5 | For example: 6 | ```js 7 | { 8 | "query": { 9 | "Name": "$contains 'Default'" 10 | }, 11 | "include": { 12 | "Children": { 13 | "query": { 14 | "Year": "= 2020" 15 | } 16 | }, 17 | "AnotherChildren": {} 18 | }, 19 | "type": "ParentType", 20 | "page": 1, 21 | "pageSize": 25 22 | } 23 | ``` 24 | will be convert to 25 | ```csharp 26 | dbContext.Parent.Include(p.Children.Where(c => c.Year == 2020)) 27 | .Include(p.AnotherChildren) 28 | .Where(p=>p.Name.Contains("Default")) 29 | .Skip(page * pageSize) 30 | .Take(pageSize); 31 | ``` 32 | 33 | Support Where, Include, ThenInclude, Take, Skip 34 | 35 | ```csharp 36 | var query = Context.CreateQuery(queryString, new List() { typeof(Parent) }).Cast(); 37 | return query.ToList(); 38 | ``` 39 | --------------------------------------------------------------------------------