├── .gitattributes ├── .gitignore ├── LICENSE ├── LambdaSql.Analyzers.Tests ├── ImmutableAnalyzerTests.cs ├── LambdaSql.Analyzers.Tests.csproj └── Verifiers │ ├── CodeFixVerifier.Helper.cs │ ├── CodeFixVerifier.cs │ ├── DiagnosticResult.cs │ ├── DiagnosticVerifier.Helper.cs │ └── DiagnosticVerifier.cs ├── LambdaSql.Analyzers ├── AssemblyInfo.cs ├── ImmutableAnalyzer.cs ├── LambdaSql.Analyzers.csproj └── tools │ ├── install.ps1 │ └── uninstall.ps1 ├── LambdaSql.UnitTests ├── Entities │ ├── Passport.cs │ └── Person.cs ├── Filter │ ├── MultitableSqlFilterTest.cs │ ├── SqlFilterFieldTest.cs │ ├── SqlFilterParameterTest.cs │ └── SqlFilterTest.cs ├── LambdaSql.UnitTests.csproj ├── MetadataProviderTest.cs ├── SqlFieldTest.cs ├── SqlJoinTest.cs ├── SqlSelectTest.cs └── SqlSelectWrapperTest.cs ├── LambdaSql.sln ├── LambdaSql ├── Exceptions.cs ├── Extensions.cs ├── Field │ ├── AggregateFunc.cs │ ├── ISqlField.cs │ ├── ITypedSqlField.cs │ └── SqlField.cs ├── Filter │ ├── ISqlFilter.cs │ ├── MultitableSqlFilter.cs │ ├── SqlFilter.cs │ ├── SqlFilterBase.cs │ ├── SqlFilterBuilder.cs │ ├── SqlFilterConfiguration.cs │ ├── SqlFilterField.cs │ └── SqlFilterItem │ │ ├── ConstSqlFilterItem.cs │ │ ├── ISqlFilterItem.cs │ │ ├── SqlFilterItem.cs │ │ ├── SqlFilterItems.cs │ │ └── SqlFilterParameter.cs ├── IMetadataProvider.cs ├── ISqlSelect.cs ├── LambdaSql.csproj ├── LibHelper.cs ├── MetadataProvider.cs ├── MsSqlServerExtensions.cs ├── Properties │ └── AssemblyInfo.cs ├── QueryBuilder │ ├── ISqlSelectQueryBuilder.cs │ ├── SqlSelectQueryBuilder.cs │ ├── SqlSelectQueryBuilderBase.cs │ └── SqlSelectWrapperQueryBuilder.cs ├── SqlAlias.cs ├── SqlAliasContainer.cs ├── SqlAliasContainerBuilder.cs ├── SqlJoin.cs ├── SqlSelect.cs ├── SqlSelectBase.cs ├── SqlSelectInfo.cs └── SqlSelectWrapper.cs └── 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 | x64/ 19 | x86/ 20 | build/ 21 | bld/ 22 | [Bb]in/ 23 | [Oo]bj/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | 28 | # MSTest test Results 29 | [Tt]est[Rr]esult*/ 30 | [Bb]uild[Ll]og.* 31 | 32 | # NUNIT 33 | *.VisualState.xml 34 | TestResult.xml 35 | 36 | # Build Results of an ATL Project 37 | [Dd]ebugPS/ 38 | [Rr]eleasePS/ 39 | dlldata.c 40 | 41 | # DNX 42 | project.lock.json 43 | artifacts/ 44 | 45 | *_i.c 46 | *_p.c 47 | *_i.h 48 | *.ilk 49 | *.meta 50 | *.obj 51 | *.pch 52 | *.pdb 53 | *.pgc 54 | *.pgd 55 | *.rsp 56 | *.sbr 57 | *.tlb 58 | *.tli 59 | *.tlh 60 | *.tmp 61 | *.tmp_proj 62 | *.log 63 | *.vspscc 64 | *.vssscc 65 | .builds 66 | *.pidb 67 | *.svclog 68 | *.scc 69 | 70 | # Chutzpah Test files 71 | _Chutzpah* 72 | 73 | # Visual C++ cache files 74 | ipch/ 75 | *.aps 76 | *.ncb 77 | *.opensdf 78 | *.sdf 79 | *.cachefile 80 | 81 | # Visual Studio profiler 82 | *.psess 83 | *.vsp 84 | *.vspx 85 | 86 | # TFS 2012 Local Workspace 87 | $tf/ 88 | 89 | # Guidance Automation Toolkit 90 | *.gpState 91 | 92 | # ReSharper is a .NET coding add-in 93 | _ReSharper*/ 94 | *.[Rr]e[Ss]harper 95 | *.DotSettings.user 96 | 97 | # JustCode is a .NET coding add-in 98 | .JustCode 99 | 100 | # TeamCity is a build add-in 101 | _TeamCity* 102 | 103 | # DotCover is a Code Coverage Tool 104 | *.dotCover 105 | 106 | # NCrunch 107 | _NCrunch_* 108 | .*crunch*.local.xml 109 | 110 | # MightyMoose 111 | *.mm.* 112 | AutoTest.Net/ 113 | 114 | # Web workbench (sass) 115 | .sass-cache/ 116 | 117 | # Installshield output folder 118 | [Ee]xpress/ 119 | 120 | # DocProject is a documentation generator add-in 121 | DocProject/buildhelp/ 122 | DocProject/Help/*.HxT 123 | DocProject/Help/*.HxC 124 | DocProject/Help/*.hhc 125 | DocProject/Help/*.hhk 126 | DocProject/Help/*.hhp 127 | DocProject/Help/Html2 128 | DocProject/Help/html 129 | 130 | # Click-Once directory 131 | publish/ 132 | 133 | # Publish Web Output 134 | *.[Pp]ublish.xml 135 | *.azurePubxml 136 | ## TODO: Comment the next line if you want to checkin your 137 | ## web deploy settings but do note that will include unencrypted 138 | ## passwords 139 | #*.pubxml 140 | 141 | *.publishproj 142 | 143 | # NuGet Packages 144 | *.nupkg 145 | # The packages folder can be ignored because of Package Restore 146 | **/packages/* 147 | # except build/, which is used as an MSBuild target. 148 | !**/packages/build/ 149 | # Uncomment if necessary however generally it will be regenerated when needed 150 | #!**/packages/repositories.config 151 | 152 | # Windows Azure Build Output 153 | csx/ 154 | *.build.csdef 155 | 156 | # Windows Store app package directory 157 | AppPackages/ 158 | 159 | # Visual Studio cache files 160 | # files ending in .cache can be ignored 161 | *.[Cc]ache 162 | # but keep track of directories ending in .cache 163 | !*.[Cc]ache/ 164 | 165 | # Others 166 | ClientBin/ 167 | [Ss]tyle[Cc]op.* 168 | ~$* 169 | *~ 170 | *.dbmdl 171 | *.dbproj.schemaview 172 | *.pfx 173 | *.publishsettings 174 | node_modules/ 175 | orleans.codegen.cs 176 | 177 | # RIA/Silverlight projects 178 | Generated_Code/ 179 | 180 | # Backup & report files from converting an old project file 181 | # to a newer Visual Studio version. Backup files are not needed, 182 | # because we have git ;-) 183 | _UpgradeReport_Files/ 184 | Backup*/ 185 | UpgradeLog*.XML 186 | UpgradeLog*.htm 187 | 188 | # SQL Server files 189 | *.mdf 190 | *.ldf 191 | 192 | # Business Intelligence projects 193 | *.rdl.data 194 | *.bim.layout 195 | *.bim_*.settings 196 | 197 | # Microsoft Fakes 198 | FakesAssemblies/ 199 | 200 | # Node.js Tools for Visual Studio 201 | .ntvs_analysis.dat 202 | 203 | # Visual Studio 6 build log 204 | *.plg 205 | 206 | # Visual Studio 6 workspace options file 207 | *.opt 208 | 209 | # LightSwitch generated files 210 | GeneratedArtifacts/ 211 | _Pvt_Extensions/ 212 | ModelManifest.xml 213 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Sergey Aseev 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LambdaSql.Analyzers.Tests/ImmutableAnalyzerTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using LambdaSql.Analyzers.Tests.Verifiers; 3 | using Microsoft.CodeAnalysis; 4 | using Microsoft.CodeAnalysis.Diagnostics; 5 | using Xunit; 6 | 7 | namespace LambdaSql.Analyzers.Tests 8 | { 9 | public class ImmutableAnalyzerTests : DiagnosticVerifier 10 | { 11 | [Theory] 12 | [InlineData("new SqlSelect().Distinct()", "LambdaSql.SqlSelect", "Distinct")] 13 | [InlineData("new SqlSelect().Distinct().Distinct(false)", "LambdaSql.SqlSelect", "Distinct")] 14 | public void InvalidExpression_DiagnosticIsReported(string expression, string type, string method) 15 | { 16 | var test = $@" 17 | namespace LambdaSql.Analyzers.Tests 18 | {{ 19 | class TestType 20 | {{ 21 | void TestMethod() 22 | {{ 23 | {expression}; 24 | }} 25 | }} 26 | }}"; 27 | var expected = new DiagnosticResult 28 | { 29 | Id = "LSql1000", 30 | Message = String.Format(ImmutableAnalyzer.MESSAGE_FORMAT, type, method), 31 | Severity = DiagnosticSeverity.Error, 32 | Locations = new[] {new DiagnosticResultLocation("Test0.cs", 8, 13)} 33 | }; 34 | VerifyCSharpDiagnostic(test, expected); 35 | } 36 | 37 | [Theory] 38 | [InlineData("var qry = new SqlSelect().Distinct()")] 39 | [InlineData("var qry = new SqlSelect().Distinct().Distinct(false)")] 40 | public void ValidExpression_NoDiagnosticIsReported(string expression) 41 | { 42 | var test = $@" 43 | namespace LambdaSql.Analyzers.Tests 44 | {{ 45 | class TestType 46 | {{ 47 | void TestMethod() 48 | {{ 49 | {expression}; 50 | }} 51 | }} 52 | }}"; 53 | 54 | VerifyCSharpDiagnostic(test); 55 | } 56 | 57 | [Theory] 58 | [InlineData("new SqlAliasContainerBuilder().Register(\"m\")")] 59 | [InlineData("MetadataProvider.Initialize(new SqlAliasContainerBuilder())")] 60 | public void MutableMethod_NoDiagnosticIsReported(string expression) 61 | { 62 | var test = $@" 63 | namespace LambdaSql.Analyzers.Tests 64 | {{ 65 | class TestType 66 | {{ 67 | void TestMethod() 68 | {{ 69 | {expression}; 70 | }} 71 | }} 72 | }}"; 73 | 74 | VerifyCSharpDiagnostic(test); 75 | } 76 | 77 | protected override DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer() => new ImmutableAnalyzer(); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /LambdaSql.Analyzers.Tests/LambdaSql.Analyzers.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | all 13 | runtime; build; native; contentfiles; analyzers; buildtransitive 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /LambdaSql.Analyzers.Tests/Verifiers/CodeFixVerifier.Helper.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Threading; 4 | using Microsoft.CodeAnalysis; 5 | using Microsoft.CodeAnalysis.CodeActions; 6 | using Microsoft.CodeAnalysis.Formatting; 7 | using Microsoft.CodeAnalysis.Simplification; 8 | 9 | namespace LambdaSql.Analyzers.Tests.Verifiers 10 | { 11 | /// 12 | /// Diagnostic Producer class with extra methods dealing with applying codefixes 13 | /// All methods are static 14 | /// 15 | public abstract partial class CodeFixVerifier : DiagnosticVerifier 16 | { 17 | /// 18 | /// Apply the inputted CodeAction to the inputted document. 19 | /// Meant to be used to apply codefixes. 20 | /// 21 | /// The Document to apply the fix on 22 | /// A CodeAction that will be applied to the Document. 23 | /// A Document with the changes from the CodeAction 24 | private static Document ApplyFix(Document document, CodeAction codeAction) 25 | { 26 | var operations = codeAction.GetOperationsAsync(CancellationToken.None).Result; 27 | var solution = operations.OfType().Single().ChangedSolution; 28 | return solution.GetDocument(document.Id); 29 | } 30 | 31 | /// 32 | /// Compare two collections of Diagnostics,and return a list of any new diagnostics that appear only in the second collection. 33 | /// Note: Considers Diagnostics to be the same if they have the same Ids. In the case of multiple diagnostics with the same Id in a row, 34 | /// this method may not necessarily return the new one. 35 | /// 36 | /// The Diagnostics that existed in the code before the CodeFix was applied 37 | /// The Diagnostics that exist in the code after the CodeFix was applied 38 | /// A list of Diagnostics that only surfaced in the code after the CodeFix was applied 39 | private static IEnumerable GetNewDiagnostics(IEnumerable diagnostics, IEnumerable newDiagnostics) 40 | { 41 | var oldArray = diagnostics.OrderBy(d => d.Location.SourceSpan.Start).ToArray(); 42 | var newArray = newDiagnostics.OrderBy(d => d.Location.SourceSpan.Start).ToArray(); 43 | 44 | int oldIndex = 0; 45 | int newIndex = 0; 46 | 47 | while (newIndex < newArray.Length) 48 | { 49 | if (oldIndex < oldArray.Length && oldArray[oldIndex].Id == newArray[newIndex].Id) 50 | { 51 | ++oldIndex; 52 | ++newIndex; 53 | } 54 | else 55 | { 56 | yield return newArray[newIndex++]; 57 | } 58 | } 59 | } 60 | 61 | /// 62 | /// Get the existing compiler diagnostics on the inputted document. 63 | /// 64 | /// The Document to run the compiler diagnostic analyzers on 65 | /// The compiler diagnostics that were found in the code 66 | private static IEnumerable GetCompilerDiagnostics(Document document) 67 | { 68 | return document.GetSemanticModelAsync().Result.GetDiagnostics(); 69 | } 70 | 71 | /// 72 | /// Given a document, turn it into a string based on the syntax root 73 | /// 74 | /// The Document to be converted to a string 75 | /// A string containing the syntax of the Document after formatting 76 | private static string GetStringFromDocument(Document document) 77 | { 78 | var simplifiedDoc = Simplifier.ReduceAsync(document, Simplifier.Annotation).Result; 79 | var root = simplifiedDoc.GetSyntaxRootAsync().Result; 80 | root = Formatter.Format(root, Formatter.Annotation, simplifiedDoc.Project.Solution.Workspace); 81 | return root.GetText().ToString(); 82 | } 83 | } 84 | } 85 | 86 | -------------------------------------------------------------------------------- /LambdaSql.Analyzers.Tests/Verifiers/CodeFixVerifier.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Threading; 4 | using Microsoft.CodeAnalysis; 5 | using Microsoft.CodeAnalysis.CodeActions; 6 | using Microsoft.CodeAnalysis.CodeFixes; 7 | using Microsoft.CodeAnalysis.Diagnostics; 8 | using Microsoft.CodeAnalysis.Formatting; 9 | using Xunit; 10 | 11 | namespace LambdaSql.Analyzers.Tests.Verifiers 12 | { 13 | /// 14 | /// Superclass of all Unit tests made for diagnostics with codefixes. 15 | /// Contains methods used to verify correctness of codefixes 16 | /// 17 | public abstract partial class CodeFixVerifier : DiagnosticVerifier 18 | { 19 | /// 20 | /// Returns the codefix being tested (C#) - to be implemented in non-abstract class 21 | /// 22 | /// The CodeFixProvider to be used for CSharp code 23 | protected virtual CodeFixProvider GetCSharpCodeFixProvider() 24 | { 25 | return null; 26 | } 27 | 28 | /// 29 | /// Returns the codefix being tested (VB) - to be implemented in non-abstract class 30 | /// 31 | /// The CodeFixProvider to be used for VisualBasic code 32 | protected virtual CodeFixProvider GetBasicCodeFixProvider() 33 | { 34 | return null; 35 | } 36 | 37 | /// 38 | /// Called to test a C# codefix when applied on the inputted string as a source 39 | /// 40 | /// A class in the form of a string before the CodeFix was applied to it 41 | /// A class in the form of a string after the CodeFix was applied to it 42 | /// Index determining which codefix to apply if there are multiple 43 | /// A bool controlling whether or not the test will fail if the CodeFix introduces other warnings after being applied 44 | protected void VerifyCSharpFix(string oldSource, string newSource, int? codeFixIndex = null, bool allowNewCompilerDiagnostics = false) 45 | { 46 | VerifyFix(LanguageNames.CSharp, GetCSharpDiagnosticAnalyzer(), GetCSharpCodeFixProvider(), oldSource, newSource, codeFixIndex, allowNewCompilerDiagnostics); 47 | } 48 | 49 | /// 50 | /// Called to test a VB codefix when applied on the inputted string as a source 51 | /// 52 | /// A class in the form of a string before the CodeFix was applied to it 53 | /// A class in the form of a string after the CodeFix was applied to it 54 | /// Index determining which codefix to apply if there are multiple 55 | /// A bool controlling whether or not the test will fail if the CodeFix introduces other warnings after being applied 56 | protected void VerifyBasicFix(string oldSource, string newSource, int? codeFixIndex = null, bool allowNewCompilerDiagnostics = false) 57 | { 58 | VerifyFix(LanguageNames.VisualBasic, GetBasicDiagnosticAnalyzer(), GetBasicCodeFixProvider(), oldSource, newSource, codeFixIndex, allowNewCompilerDiagnostics); 59 | } 60 | 61 | /// 62 | /// General verifier for codefixes. 63 | /// Creates a Document from the source string, then gets diagnostics on it and applies the relevant codefixes. 64 | /// Then gets the string after the codefix is applied and compares it with the expected result. 65 | /// Note: If any codefix causes new diagnostics to show up, the test fails unless allowNewCompilerDiagnostics is set to true. 66 | /// 67 | /// The language the source code is in 68 | /// The analyzer to be applied to the source code 69 | /// The codefix to be applied to the code wherever the relevant Diagnostic is found 70 | /// A class in the form of a string before the CodeFix was applied to it 71 | /// A class in the form of a string after the CodeFix was applied to it 72 | /// Index determining which codefix to apply if there are multiple 73 | /// A bool controlling whether or not the test will fail if the CodeFix introduces other warnings after being applied 74 | private void VerifyFix(string language, DiagnosticAnalyzer analyzer, CodeFixProvider codeFixProvider, string oldSource, string newSource, int? codeFixIndex, bool allowNewCompilerDiagnostics) 75 | { 76 | var document = CreateDocument(oldSource, language); 77 | var analyzerDiagnostics = GetSortedDiagnosticsFromDocuments(analyzer, new[] { document }); 78 | var compilerDiagnostics = GetCompilerDiagnostics(document); 79 | var attempts = analyzerDiagnostics.Length; 80 | 81 | for (int i = 0; i < attempts; ++i) 82 | { 83 | var actions = new List(); 84 | var context = new CodeFixContext(document, analyzerDiagnostics[0], (a, d) => actions.Add(a), CancellationToken.None); 85 | codeFixProvider.RegisterCodeFixesAsync(context).Wait(); 86 | 87 | if (!actions.Any()) 88 | { 89 | break; 90 | } 91 | 92 | if (codeFixIndex != null) 93 | { 94 | document = ApplyFix(document, actions.ElementAt((int)codeFixIndex)); 95 | break; 96 | } 97 | 98 | document = ApplyFix(document, actions.ElementAt(0)); 99 | analyzerDiagnostics = GetSortedDiagnosticsFromDocuments(analyzer, new[] { document }); 100 | 101 | var newCompilerDiagnostics = GetNewDiagnostics(compilerDiagnostics, GetCompilerDiagnostics(document)); 102 | 103 | //check if applying the code fix introduced any new compiler diagnostics 104 | if (!allowNewCompilerDiagnostics && newCompilerDiagnostics.Any()) 105 | { 106 | // Format and get the compiler diagnostics again so that the locations make sense in the output 107 | document = document.WithSyntaxRoot(Formatter.Format(document.GetSyntaxRootAsync().Result, Formatter.Annotation, document.Project.Solution.Workspace)); 108 | newCompilerDiagnostics = GetNewDiagnostics(compilerDiagnostics, GetCompilerDiagnostics(document)); 109 | 110 | Assert.True(false, 111 | string.Format("Fix introduced new compiler diagnostics:\r\n{0}\r\n\r\nNew document:\r\n{1}\r\n", 112 | string.Join("\r\n", newCompilerDiagnostics.Select(d => d.ToString())), 113 | document.GetSyntaxRootAsync().Result.ToFullString())); 114 | } 115 | 116 | //check if there are analyzer diagnostics left after the code fix 117 | if (!analyzerDiagnostics.Any()) 118 | { 119 | break; 120 | } 121 | } 122 | 123 | //after applying all of the code fixes, compare the resulting string to the inputted one 124 | var actual = GetStringFromDocument(document); 125 | Assert.Equal(newSource, actual); 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /LambdaSql.Analyzers.Tests/Verifiers/DiagnosticResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.CodeAnalysis; 3 | 4 | namespace LambdaSql.Analyzers.Tests.Verifiers 5 | { 6 | /// 7 | /// Location where the diagnostic appears, as determined by path, line number, and column number. 8 | /// 9 | public struct DiagnosticResultLocation 10 | { 11 | public DiagnosticResultLocation(string path, int line, int column) 12 | { 13 | if (line < -1) 14 | { 15 | throw new ArgumentOutOfRangeException(nameof(line), "line must be >= -1"); 16 | } 17 | 18 | if (column < -1) 19 | { 20 | throw new ArgumentOutOfRangeException(nameof(column), "column must be >= -1"); 21 | } 22 | 23 | this.Path = path; 24 | this.Line = line; 25 | this.Column = column; 26 | } 27 | 28 | public string Path { get; } 29 | public int Line { get; } 30 | public int Column { get; } 31 | } 32 | 33 | /// 34 | /// Struct that stores information about a Diagnostic appearing in a source 35 | /// 36 | public struct DiagnosticResult 37 | { 38 | private DiagnosticResultLocation[] locations; 39 | 40 | public DiagnosticResultLocation[] Locations 41 | { 42 | get 43 | { 44 | if (this.locations == null) 45 | { 46 | this.locations = new DiagnosticResultLocation[] { }; 47 | } 48 | return this.locations; 49 | } 50 | 51 | set 52 | { 53 | this.locations = value; 54 | } 55 | } 56 | 57 | public DiagnosticSeverity Severity { get; set; } 58 | 59 | public string Id { get; set; } 60 | 61 | public string Message { get; set; } 62 | 63 | public string Path 64 | { 65 | get 66 | { 67 | return this.Locations.Length > 0 ? this.Locations[0].Path : ""; 68 | } 69 | } 70 | 71 | public int Line 72 | { 73 | get 74 | { 75 | return this.Locations.Length > 0 ? this.Locations[0].Line : -1; 76 | } 77 | } 78 | 79 | public int Column 80 | { 81 | get 82 | { 83 | return this.Locations.Length > 0 ? this.Locations[0].Column : -1; 84 | } 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /LambdaSql.Analyzers.Tests/Verifiers/DiagnosticVerifier.Helper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.Immutable; 4 | using System.Linq; 5 | using Microsoft.CodeAnalysis; 6 | using Microsoft.CodeAnalysis.CSharp; 7 | using Microsoft.CodeAnalysis.Diagnostics; 8 | using Microsoft.CodeAnalysis.Text; 9 | 10 | namespace LambdaSql.Analyzers.Tests.Verifiers 11 | { 12 | /// 13 | /// Class for turning strings into documents and getting the diagnostics on them 14 | /// All methods are static 15 | /// 16 | public abstract partial class DiagnosticVerifier 17 | { 18 | private static readonly MetadataReference CorlibReference = MetadataReference.CreateFromFile(typeof(object).Assembly.Location); 19 | private static readonly MetadataReference SystemCoreReference = MetadataReference.CreateFromFile(typeof(Enumerable).Assembly.Location); 20 | private static readonly MetadataReference CSharpSymbolsReference = MetadataReference.CreateFromFile(typeof(CSharpCompilation).Assembly.Location); 21 | private static readonly MetadataReference CodeAnalysisReference = MetadataReference.CreateFromFile(typeof(Compilation).Assembly.Location); 22 | private static readonly MetadataReference LambdaSqlReference = MetadataReference.CreateFromFile(typeof(SqlSelect).Assembly.Location); 23 | 24 | internal static string DefaultFilePathPrefix = "Test"; 25 | internal static string CSharpDefaultFileExt = "cs"; 26 | internal static string VisualBasicDefaultExt = "vb"; 27 | internal static string TestProjectName = "TestProject"; 28 | 29 | #region Get Diagnostics 30 | 31 | /// 32 | /// Given classes in the form of strings, their language, and an IDiagnosticAnalyzer to apply to it, return the diagnostics found in the string after converting it to a document. 33 | /// 34 | /// Classes in the form of strings 35 | /// The language the source classes are in 36 | /// The analyzer to be run on the sources 37 | /// An IEnumerable of Diagnostics that surfaced in the source code, sorted by Location 38 | private static Diagnostic[] GetSortedDiagnostics(string[] sources, string language, DiagnosticAnalyzer analyzer) 39 | { 40 | return GetSortedDiagnosticsFromDocuments(analyzer, GetDocuments(sources, language)); 41 | } 42 | 43 | /// 44 | /// Given an analyzer and a document to apply it to, run the analyzer and gather an array of diagnostics found in it. 45 | /// The returned diagnostics are then ordered by location in the source document. 46 | /// 47 | /// The analyzer to run on the documents 48 | /// The Documents that the analyzer will be run on 49 | /// An IEnumerable of Diagnostics that surfaced in the source code, sorted by Location 50 | protected static Diagnostic[] GetSortedDiagnosticsFromDocuments(DiagnosticAnalyzer analyzer, Document[] documents) 51 | { 52 | var projects = new HashSet(); 53 | foreach (var document in documents) 54 | { 55 | projects.Add(document.Project); 56 | } 57 | 58 | var diagnostics = new List(); 59 | foreach (var project in projects) 60 | { 61 | var compilationWithAnalyzers = project.GetCompilationAsync().Result.WithAnalyzers(ImmutableArray.Create(analyzer)); 62 | var diags = compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync().Result; 63 | foreach (var diag in diags) 64 | { 65 | if (diag.Location == Location.None || diag.Location.IsInMetadata) 66 | { 67 | diagnostics.Add(diag); 68 | } 69 | else 70 | { 71 | for (int i = 0; i < documents.Length; i++) 72 | { 73 | var document = documents[i]; 74 | var tree = document.GetSyntaxTreeAsync().Result; 75 | if (tree == diag.Location.SourceTree) 76 | { 77 | diagnostics.Add(diag); 78 | } 79 | } 80 | } 81 | } 82 | } 83 | 84 | var results = SortDiagnostics(diagnostics); 85 | diagnostics.Clear(); 86 | return results; 87 | } 88 | 89 | /// 90 | /// Sort diagnostics by location in source document 91 | /// 92 | /// The list of Diagnostics to be sorted 93 | /// An IEnumerable containing the Diagnostics in order of Location 94 | private static Diagnostic[] SortDiagnostics(IEnumerable diagnostics) 95 | { 96 | return diagnostics.OrderBy(d => d.Location.SourceSpan.Start).ToArray(); 97 | } 98 | 99 | #endregion 100 | 101 | #region Set up compilation and documents 102 | /// 103 | /// Given an array of strings as sources and a language, turn them into a project and return the documents and spans of it. 104 | /// 105 | /// Classes in the form of strings 106 | /// The language the source code is in 107 | /// A Tuple containing the Documents produced from the sources and their TextSpans if relevant 108 | private static Document[] GetDocuments(string[] sources, string language) 109 | { 110 | if (language != LanguageNames.CSharp && language != LanguageNames.VisualBasic) 111 | { 112 | throw new ArgumentException("Unsupported Language"); 113 | } 114 | 115 | var project = CreateProject(sources, language); 116 | var documents = project.Documents.ToArray(); 117 | 118 | if (sources.Length != documents.Length) 119 | { 120 | throw new InvalidOperationException("Amount of sources did not match amount of Documents created"); 121 | } 122 | 123 | return documents; 124 | } 125 | 126 | /// 127 | /// Create a Document from a string through creating a project that contains it. 128 | /// 129 | /// Classes in the form of a string 130 | /// The language the source code is in 131 | /// A Document created from the source string 132 | protected static Document CreateDocument(string source, string language = LanguageNames.CSharp) 133 | { 134 | return CreateProject(new[] { source }, language).Documents.First(); 135 | } 136 | 137 | /// 138 | /// Create a project using the inputted strings as sources. 139 | /// 140 | /// Classes in the form of strings 141 | /// The language the source code is in 142 | /// A Project created out of the Documents created from the source strings 143 | private static Project CreateProject(string[] sources, string language = LanguageNames.CSharp) 144 | { 145 | string fileNamePrefix = DefaultFilePathPrefix; 146 | string fileExt = language == LanguageNames.CSharp ? CSharpDefaultFileExt : VisualBasicDefaultExt; 147 | 148 | var projectId = ProjectId.CreateNewId(debugName: TestProjectName); 149 | 150 | var solution = new AdhocWorkspace() 151 | .CurrentSolution 152 | .AddProject(projectId, TestProjectName, TestProjectName, language) 153 | .AddMetadataReference(projectId, CorlibReference) 154 | .AddMetadataReference(projectId, SystemCoreReference) 155 | .AddMetadataReference(projectId, CSharpSymbolsReference) 156 | .AddMetadataReference(projectId, CodeAnalysisReference) 157 | .AddMetadataReference(projectId, LambdaSqlReference); 158 | 159 | int count = 0; 160 | foreach (var source in sources) 161 | { 162 | var newFileName = fileNamePrefix + count + "." + fileExt; 163 | var documentId = DocumentId.CreateNewId(projectId, debugName: newFileName); 164 | solution = solution.AddDocument(documentId, newFileName, SourceText.From(source)); 165 | count++; 166 | } 167 | return solution.GetProject(projectId); 168 | } 169 | #endregion 170 | } 171 | } 172 | 173 | -------------------------------------------------------------------------------- /LambdaSql.Analyzers/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | [assembly: InternalsVisibleTo("LambdaSql.Analyzers.Tests")] 3 | -------------------------------------------------------------------------------- /LambdaSql.Analyzers/ImmutableAnalyzer.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | using System.Linq; 3 | using Microsoft.CodeAnalysis; 4 | using Microsoft.CodeAnalysis.CSharp; 5 | using Microsoft.CodeAnalysis.CSharp.Syntax; 6 | using Microsoft.CodeAnalysis.Diagnostics; 7 | 8 | namespace LambdaSql.Analyzers 9 | { 10 | [DiagnosticAnalyzer(LanguageNames.CSharp)] 11 | public class ImmutableAnalyzer : DiagnosticAnalyzer 12 | { 13 | internal const string MESSAGE_FORMAT = "'{0}' is immutable and '{1}' will not have any effect on it. Consider using the return value from '{1}'."; 14 | 15 | private static readonly DiagnosticDescriptor _rule = new DiagnosticDescriptor("LSql1000", 16 | "Do not ignore values returned by methods on immutable objects.", 17 | MESSAGE_FORMAT, 18 | "Immutability", 19 | DiagnosticSeverity.Error, isEnabledByDefault: true); 20 | 21 | private static readonly string[] _mutableTypes = new[] { "LambdaSql.SqlAliasContainerBuilder", "LambdaSql.MetadataProvider" }; 22 | 23 | public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(_rule); 24 | 25 | public override void Initialize(AnalysisContext context) 26 | { 27 | context.RegisterCompilationStartAction(compilationStartContext => 28 | { 29 | var lambdaSqlAssembly = compilationStartContext.Compilation.GetTypeByMetadataName("LambdaSql.SqlSelect")?.ContainingAssembly; 30 | compilationStartContext.RegisterSyntaxNodeAction(analysisContext => 31 | Analyze(analysisContext, lambdaSqlAssembly), SyntaxKind.InvocationExpression); 32 | }); 33 | } 34 | 35 | private void Analyze(SyntaxNodeAnalysisContext context, IAssemblySymbol lambdaSqlAssembly) 36 | { 37 | var invocation = (InvocationExpressionSyntax)context.Node; 38 | var symbolInfo = context.SemanticModel.GetSymbolInfo(invocation, context.CancellationToken).Symbol; 39 | if (symbolInfo != null && ReferenceEquals(symbolInfo.ContainingAssembly, lambdaSqlAssembly) 40 | && invocation.Parent.IsKind(SyntaxKind.ExpressionStatement) && IsImmutableMember(symbolInfo)) 41 | { 42 | var diagnostic = Diagnostic.Create(_rule, invocation.GetLocation(), 43 | symbolInfo.ContainingType.ToString(), symbolInfo.Name); 44 | context.ReportDiagnostic(diagnostic); 45 | } 46 | } 47 | 48 | private bool IsImmutableMember(ISymbol symbol) 49 | { 50 | var typeName = symbol.ContainingType.ToString(); 51 | return _mutableTypes.All(type => type != typeName); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /LambdaSql.Analyzers/LambdaSql.Analyzers.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | True 6 | True 7 | true 8 | true 9 | snupkg 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | LambdaSql.Analyzers 19 | 1.3.0.0 20 | 1.3.0.0 21 | 1.3.0.0 22 | 1.3.0.0 23 | Serg046 24 | https://github.com/Serg046/LambdaSql 25 | false 26 | LambdaSql.Analyzers 27 | LambdaSql.Analyzers, LambdaSql 28 | true 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /LambdaSql.Analyzers/tools/install.ps1: -------------------------------------------------------------------------------- 1 | param($installPath, $toolsPath, $package, $project) 2 | 3 | if($project.Object.SupportsPackageDependencyResolution) 4 | { 5 | if($project.Object.SupportsPackageDependencyResolution()) 6 | { 7 | # Do not install analyzers via install.ps1, instead let the project system handle it. 8 | return 9 | } 10 | } 11 | 12 | $analyzersPaths = Join-Path (Join-Path (Split-Path -Path $toolsPath -Parent) "analyzers") * -Resolve 13 | 14 | foreach($analyzersPath in $analyzersPaths) 15 | { 16 | if (Test-Path $analyzersPath) 17 | { 18 | # Install the language agnostic analyzers. 19 | foreach ($analyzerFilePath in Get-ChildItem -Path "$analyzersPath\*.dll" -Exclude *.resources.dll) 20 | { 21 | if($project.Object.AnalyzerReferences) 22 | { 23 | $project.Object.AnalyzerReferences.Add($analyzerFilePath.FullName) 24 | } 25 | } 26 | } 27 | } 28 | 29 | # $project.Type gives the language name like (C# or VB.NET) 30 | $languageFolder = "" 31 | if($project.Type -eq "C#") 32 | { 33 | $languageFolder = "cs" 34 | } 35 | if($project.Type -eq "VB.NET") 36 | { 37 | $languageFolder = "vb" 38 | } 39 | if($languageFolder -eq "") 40 | { 41 | return 42 | } 43 | 44 | foreach($analyzersPath in $analyzersPaths) 45 | { 46 | # Install language specific analyzers. 47 | $languageAnalyzersPath = join-path $analyzersPath $languageFolder 48 | if (Test-Path $languageAnalyzersPath) 49 | { 50 | foreach ($analyzerFilePath in Get-ChildItem -Path "$languageAnalyzersPath\*.dll" -Exclude *.resources.dll) 51 | { 52 | if($project.Object.AnalyzerReferences) 53 | { 54 | $project.Object.AnalyzerReferences.Add($analyzerFilePath.FullName) 55 | } 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /LambdaSql.Analyzers/tools/uninstall.ps1: -------------------------------------------------------------------------------- 1 | param($installPath, $toolsPath, $package, $project) 2 | 3 | if($project.Object.SupportsPackageDependencyResolution) 4 | { 5 | if($project.Object.SupportsPackageDependencyResolution()) 6 | { 7 | # Do not uninstall analyzers via uninstall.ps1, instead let the project system handle it. 8 | return 9 | } 10 | } 11 | 12 | $analyzersPaths = Join-Path (Join-Path (Split-Path -Path $toolsPath -Parent) "analyzers") * -Resolve 13 | 14 | foreach($analyzersPath in $analyzersPaths) 15 | { 16 | # Uninstall the language agnostic analyzers. 17 | if (Test-Path $analyzersPath) 18 | { 19 | foreach ($analyzerFilePath in Get-ChildItem -Path "$analyzersPath\*.dll" -Exclude *.resources.dll) 20 | { 21 | if($project.Object.AnalyzerReferences) 22 | { 23 | $project.Object.AnalyzerReferences.Remove($analyzerFilePath.FullName) 24 | } 25 | } 26 | } 27 | } 28 | 29 | # $project.Type gives the language name like (C# or VB.NET) 30 | $languageFolder = "" 31 | if($project.Type -eq "C#") 32 | { 33 | $languageFolder = "cs" 34 | } 35 | if($project.Type -eq "VB.NET") 36 | { 37 | $languageFolder = "vb" 38 | } 39 | if($languageFolder -eq "") 40 | { 41 | return 42 | } 43 | 44 | foreach($analyzersPath in $analyzersPaths) 45 | { 46 | # Uninstall language specific analyzers. 47 | $languageAnalyzersPath = join-path $analyzersPath $languageFolder 48 | if (Test-Path $languageAnalyzersPath) 49 | { 50 | foreach ($analyzerFilePath in Get-ChildItem -Path "$languageAnalyzersPath\*.dll" -Exclude *.resources.dll) 51 | { 52 | if($project.Object.AnalyzerReferences) 53 | { 54 | try 55 | { 56 | $project.Object.AnalyzerReferences.Remove($analyzerFilePath.FullName) 57 | } 58 | catch 59 | { 60 | 61 | } 62 | } 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /LambdaSql.UnitTests/Entities/Passport.cs: -------------------------------------------------------------------------------- 1 | namespace LambdaSql.UnitTests.Entities 2 | { 3 | public class Passport 4 | { 5 | public int Id { get; set; } 6 | public int PersonId { get; set; } 7 | public string Number { get; set; } 8 | public int? NullablePersonId { get; set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /LambdaSql.UnitTests/Entities/Person.cs: -------------------------------------------------------------------------------- 1 | namespace LambdaSql.UnitTests.Entities 2 | { 3 | public class Person 4 | { 5 | public int Id { get; set; } 6 | public string Name { get; set; } 7 | public string LastName { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /LambdaSql.UnitTests/Filter/MultitableSqlFilterTest.cs: -------------------------------------------------------------------------------- 1 | using LambdaSql.Field; 2 | using LambdaSql.Filter; 3 | using LambdaSql.UnitTests.Entities; 4 | using Xunit; 5 | 6 | namespace LambdaSql.UnitTests.Filter 7 | { 8 | public class MultitableSqlFilterTest 9 | { 10 | [Fact] 11 | public void And_LambdaExpression_Success() 12 | { 13 | var filter = SqlFilter.Empty.And(SqlFilter.From(m => m.Id).EqualTo(5)) 14 | .And(m => m.Name).EqualTo("Sergey"); 15 | 16 | Assert.Equal("pa.Id = 5 AND pe.Name = 'Sergey'", filter.RawSql); 17 | } 18 | 19 | [Fact] 20 | public void And_EmptyFilterWithLambdaExpression_Success() 21 | { 22 | var filter = SqlFilter.Empty.And(SqlFilter.Empty) 23 | .And(m => m.Name).EqualTo("Sergey"); 24 | 25 | Assert.Equal("pe.Name = 'Sergey'", filter.RawSql); 26 | } 27 | 28 | [Fact] 29 | public void And_LambdaExpressionWithAlias_Success() 30 | { 31 | var filter = SqlFilter.Empty.And(SqlFilter.From(m => m.Id).EqualTo(5)) 32 | .And(m => m.Name, new SqlAlias("al")).EqualTo("Sergey"); 33 | 34 | Assert.Equal("pa.Id = 5 AND al.Name = 'Sergey'", filter.RawSql); 35 | } 36 | 37 | [Fact] 38 | public void And_GenericSqlField_Success() 39 | { 40 | var filter = SqlFilter.Empty.And(SqlFilter.From(m => m.Id).EqualTo(5)) 41 | .And(SqlField.From(m => m.Name)).EqualTo("Sergey"); 42 | 43 | Assert.Equal("pa.Id = 5 AND pe.Name = 'Sergey'", filter.RawSql); 44 | } 45 | 46 | [Fact] 47 | public void And_EmptyFilterWithGenericSqlField_Success() 48 | { 49 | var filter = SqlFilter.Empty.And(SqlFilter.Empty) 50 | .And(SqlField.From(m => m.Name)).EqualTo("Sergey"); 51 | 52 | Assert.Equal("pe.Name = 'Sergey'", filter.RawSql); 53 | } 54 | 55 | [Fact] 56 | public void And_TypedSqlField_Success() 57 | { 58 | var sqlField = new TypedSqlField(typeof(Person), typeof(string)) 59 | { 60 | Name = nameof(Person.Name), 61 | Alias = new SqlAlias("pe") 62 | }; 63 | var filter = SqlFilter.Empty.And(SqlFilter.From(m => m.Id).EqualTo(5)) 64 | .And(sqlField).EqualTo("Sergey"); 65 | 66 | Assert.Equal("pa.Id = 5 AND pe.Name = 'Sergey'", filter.RawSql); 67 | } 68 | 69 | [Fact] 70 | public void And_EmptyFilterWithTypedSqlField_Success() 71 | { 72 | var sqlField = new TypedSqlField(typeof(Person), typeof(string)) 73 | { 74 | Name = nameof(Person.Name), 75 | Alias = new SqlAlias("pe") 76 | }; 77 | var filter = SqlFilter.Empty.And(SqlFilter.Empty) 78 | .And(sqlField).EqualTo("Sergey"); 79 | 80 | Assert.Equal("pe.Name = 'Sergey'", filter.RawSql); 81 | } 82 | 83 | [Fact] 84 | public void And_SqlFilter_Success() 85 | { 86 | var filter = SqlFilter.From(m => m.Id).EqualTo(5); 87 | 88 | Assert.Equal("pe.Id = 5 AND pe.Id = 5", SqlFilter.Empty.And(filter).And(filter).RawSql); 89 | } 90 | 91 | [Fact] 92 | public void And_EmptyFilterWithSqlFilter_Success() 93 | { 94 | var filter = SqlFilter.From(m => m.Id).EqualTo(5); 95 | 96 | Assert.Equal("pe.Id = 5", SqlFilter.Empty.And(SqlFilter.Empty).And(filter).RawSql); 97 | } 98 | 99 | [Fact] 100 | public void And_SqlFilterBase_Success() 101 | { 102 | var filter = SqlFilter.From(m => m.Id).EqualTo(5); 103 | var filter2 = SqlFilter.From(m => m.Id).EqualTo(6); 104 | 105 | Assert.Equal("pe.Id = 5 AND pa.Id = 6", SqlFilter.Empty.And(filter).And(filter2).RawSql); 106 | } 107 | 108 | [Fact] 109 | public void And_EmptyFilterWithSqlFilterBase_Success() 110 | { 111 | var filter = SqlFilter.From(m => m.Id).EqualTo(6); 112 | 113 | Assert.Equal("pa.Id = 6", SqlFilter.Empty.And(SqlFilter.Empty).And(filter).RawSql); 114 | } 115 | 116 | [Fact] 117 | public void Or_LambdaExpression_Success() 118 | { 119 | var filter = SqlFilter.Empty.And(SqlFilter.From(m => m.Id).EqualTo(5)) 120 | .Or(m => m.Name).EqualTo("Sergey"); 121 | 122 | Assert.Equal("pa.Id = 5 OR pe.Name = 'Sergey'", filter.RawSql); 123 | } 124 | 125 | [Fact] 126 | public void Or_EmptyFilterWithLambdaExpression_Success() 127 | { 128 | var filter = SqlFilter.Empty.And(SqlFilter.Empty) 129 | .Or(m => m.Name).EqualTo("Sergey"); 130 | 131 | Assert.Equal("pe.Name = 'Sergey'", filter.RawSql); 132 | } 133 | 134 | [Fact] 135 | public void Or_LambdaExpressionWithAlias_Success() 136 | { 137 | var filter = SqlFilter.Empty.And(SqlFilter.From(m => m.Id).EqualTo(5)) 138 | .Or(m => m.Name, new SqlAlias("al")).EqualTo("Sergey"); 139 | 140 | Assert.Equal("pa.Id = 5 OR al.Name = 'Sergey'", filter.RawSql); 141 | } 142 | 143 | [Fact] 144 | public void Or_GenericSqlField_Success() 145 | { 146 | var filter = SqlFilter.Empty.And(SqlFilter.From(m => m.Id).EqualTo(5)) 147 | .Or(SqlField.From(m => m.Name)).EqualTo("Sergey"); 148 | 149 | Assert.Equal("pa.Id = 5 OR pe.Name = 'Sergey'", filter.RawSql); 150 | } 151 | 152 | [Fact] 153 | public void Or_EmptyFilterWithGenericSqlField_Success() 154 | { 155 | var filter = SqlFilter.Empty.And(SqlFilter.Empty) 156 | .Or(SqlField.From(m => m.Name)).EqualTo("Sergey"); 157 | 158 | Assert.Equal("pe.Name = 'Sergey'", filter.RawSql); 159 | } 160 | 161 | [Fact] 162 | public void Or_TypedSqlField_Success() 163 | { 164 | var sqlField = new TypedSqlField(typeof(Person), typeof(string)) 165 | { 166 | Name = nameof(Person.Name), 167 | Alias = new SqlAlias("pe") 168 | }; 169 | var filter = SqlFilter.Empty.And(SqlFilter.From(m => m.Id).EqualTo(5)) 170 | .Or(sqlField).EqualTo("Sergey"); 171 | 172 | Assert.Equal("pa.Id = 5 OR pe.Name = 'Sergey'", filter.RawSql); 173 | } 174 | 175 | [Fact] 176 | public void Or_EmptyFilterWithTypedSqlField_Success() 177 | { 178 | var sqlField = new TypedSqlField(typeof(Person), typeof(string)) 179 | { 180 | Name = nameof(Person.Name), 181 | Alias = new SqlAlias("pe") 182 | }; 183 | var filter = SqlFilter.Empty.And(SqlFilter.Empty) 184 | .Or(sqlField).EqualTo("Sergey"); 185 | 186 | Assert.Equal("pe.Name = 'Sergey'", filter.RawSql); 187 | } 188 | 189 | [Fact] 190 | public void Or_SqlFilter_Success() 191 | { 192 | var filter = SqlFilter.From(m => m.Id).EqualTo(5); 193 | 194 | Assert.Equal("pe.Id = 5 OR pe.Id = 5", SqlFilter.Empty.And(filter).Or(filter).RawSql); 195 | } 196 | 197 | [Fact] 198 | public void Or_EmptyFilterWithSqlFilter_Success() 199 | { 200 | var filter = SqlFilter.From(m => m.Id).EqualTo(5); 201 | 202 | Assert.Equal("pe.Id = 5", SqlFilter.Empty.And(SqlFilter.Empty).Or(filter).RawSql); 203 | } 204 | 205 | [Fact] 206 | public void Or_SqlFilterBase_Success() 207 | { 208 | var filter = SqlFilter.From(m => m.Id).EqualTo(5); 209 | var filter2 = SqlFilter.From(m => m.Id).EqualTo(6); 210 | 211 | Assert.Equal("pe.Id = 5 OR pa.Id = 6", SqlFilter.Empty.And(filter).Or(filter2).RawSql); 212 | } 213 | 214 | [Fact] 215 | public void Or_EmptyFilterWithSqlFilterBase_Success() 216 | { 217 | var filter = SqlFilter.From(m => m.Id).EqualTo(6); 218 | 219 | Assert.Equal("pa.Id = 6", SqlFilter.Empty.And(SqlFilter.Empty).Or(filter).RawSql); 220 | } 221 | 222 | [Fact] 223 | public void AndGroup_SqlFilter_Success() 224 | { 225 | var filter = SqlFilter.From(m => m.Id).EqualTo(5); 226 | 227 | Assert.Equal("pe.Id = 5 AND (pe.Id = 5)", SqlFilter.Empty.And(filter).AndGroup(filter).RawSql); 228 | } 229 | 230 | [Fact] 231 | public void AndGroup_EmptyFilterWithSqlFilter_Success() 232 | { 233 | var filter = SqlFilter.From(m => m.Id).EqualTo(5); 234 | 235 | Assert.Equal("(pe.Id = 5)", SqlFilter.Empty.And(SqlFilter.Empty).AndGroup(filter).RawSql); 236 | } 237 | 238 | [Fact] 239 | public void AndGroup_SqlFilterBase_Success() 240 | { 241 | var filter = SqlFilter.From(m => m.Id).EqualTo(5); 242 | var filter2 = SqlFilter.From(m => m.Id).EqualTo(6); 243 | 244 | Assert.Equal("pe.Id = 5 AND (pa.Id = 6)", SqlFilter.Empty.And(filter).AndGroup(filter2).RawSql); 245 | } 246 | 247 | [Fact] 248 | public void AndGroup_EmptyFilterWithSqlFilterBase_Success() 249 | { 250 | var filter = SqlFilter.From(m => m.Id).EqualTo(6); 251 | 252 | Assert.Equal("(pa.Id = 6)", SqlFilter.Empty.And(SqlFilter.Empty).AndGroup(filter).RawSql); 253 | } 254 | 255 | [Fact] 256 | public void OrGroup_SqlFilter_Success() 257 | { 258 | var filter = SqlFilter.From(m => m.Id).EqualTo(5); 259 | 260 | Assert.Equal("pe.Id = 5 OR (pe.Id = 5)", SqlFilter.Empty.And(filter).OrGroup(filter).RawSql); 261 | } 262 | 263 | [Fact] 264 | public void OrGroup_EmptyFilterWithSqlFilter_Success() 265 | { 266 | var filter = SqlFilter.From(m => m.Id).EqualTo(5); 267 | 268 | Assert.Equal("(pe.Id = 5)", SqlFilter.Empty.And(SqlFilter.Empty).OrGroup(filter).RawSql); 269 | } 270 | 271 | [Fact] 272 | public void OrGroup_SqlFilterBase_Success() 273 | { 274 | var filter = SqlFilter.From(m => m.Id).EqualTo(5); 275 | var filter2 = SqlFilter.From(m => m.Id).EqualTo(6); 276 | 277 | Assert.Equal("pe.Id = 5 OR (pa.Id = 6)", SqlFilter.Empty.And(filter).OrGroup(filter2).RawSql); 278 | } 279 | 280 | [Fact] 281 | public void OrGroup_EmptyFilterWithSqlFilterBase_Success() 282 | { 283 | var filter = SqlFilter.From(m => m.Id).EqualTo(6); 284 | 285 | Assert.Equal("(pa.Id = 6)", SqlFilter.Empty.And(SqlFilter.Empty).OrGroup(filter).RawSql); 286 | } 287 | 288 | //--------------------------------------------------------------------------------------------------- 289 | 290 | [Fact] 291 | public void And_Lambda_InstanceIsImmutable() 292 | { 293 | var filter = SqlFilter.Empty.And(SqlFilter.From(m => m.Id).EqualTo(5)); 294 | 295 | Assert.Equal("pa.Id = 5 AND pe.Name = 'Sergey'", filter.And(m => m.Name).EqualTo("Sergey").RawSql); 296 | Assert.Equal("pa.Id = 5", filter.RawSql); 297 | } 298 | 299 | 300 | [Fact] 301 | public void And_WithParameterPrefix_Success() 302 | { 303 | var filter = SqlFilter.Empty.And(SqlFilter.From(m => m.Id).EqualTo(5)); 304 | 305 | Assert.Equal("pa.Id = @p0", filter.ParametricSql); 306 | Assert.Equal("pa.Id = @prm0", filter.WithParameterPrefix("prm").ParametricSql); 307 | } 308 | } 309 | } 310 | -------------------------------------------------------------------------------- /LambdaSql.UnitTests/Filter/SqlFilterFieldTest.cs: -------------------------------------------------------------------------------- 1 | using LambdaSql.Field; 2 | using LambdaSql.Filter; 3 | using LambdaSql.UnitTests.Entities; 4 | using Xunit; 5 | 6 | namespace LambdaSql.UnitTests.Filter 7 | { 8 | public class SqlFilterFieldTest 9 | { 10 | [Fact] 11 | public void SatisfyLambda() 12 | { 13 | var filter = SqlFilter.From(m => m.Id).SatisfyLambda(field => $"{field} IN (5)"); 14 | Assert.Equal("pe.Id IN (5)", filter.RawSql); 15 | } 16 | 17 | [Fact] 18 | public void IsNull() 19 | { 20 | Assert.Equal("pe.Id IS NULL", SqlFilter.From(m => m.Id).IsNull().RawSql); 21 | } 22 | 23 | [Fact] 24 | public void IsNotNull() 25 | { 26 | Assert.Equal("pe.Name IS NOT NULL", SqlFilter.From(m => m.Name).IsNotNull().RawSql); 27 | } 28 | 29 | [Fact] 30 | public void Like() 31 | { 32 | Assert.Equal("pe.Name LIKE '%template'", SqlFilter.From(m => m.Name).Like("%template").RawSql); 33 | } 34 | 35 | [Fact] 36 | public void EqualTo() 37 | { 38 | Assert.Equal("pe.Id = 5", SqlFilter.From(m => m.Id).EqualTo(5).RawSql); 39 | Assert.Equal("pe.Name = pe.LastName", SqlFilter.From(m => m.Name).EqualTo(m => m.LastName).RawSql); 40 | Assert.Equal("pe.Id = pa.PersonId", SqlFilter.From(m => m.Id).EqualTo(m => m.PersonId).RawSql); 41 | 42 | var peAlias = new SqlAlias("per"); 43 | var paAlias = new SqlAlias("pas"); 44 | Assert.Equal("pe.Name = per.LastName", SqlFilter.From(m => m.Name).EqualTo(m => m.LastName, peAlias).RawSql); 45 | Assert.Equal("pe.Id = pas.PersonId", SqlFilter.From(m => m.Id).EqualTo(m => m.PersonId, paAlias).RawSql); 46 | 47 | Assert.Equal("pe.Id = COUNT(pa.Id)", SqlFilter.From(p => p.Id).EqualTo(SqlField.Count(p => p.Id)).RawSql); 48 | var alias = new SqlAlias("pasp"); 49 | Assert.Equal("pe.Id = COUNT(pasp.Id)", SqlFilter.From(p => p.Id).EqualTo(SqlField.Count(alias, p => p.Id)).ToString()); 50 | } 51 | 52 | [Fact] 53 | public void NotEqualTo() 54 | { 55 | Assert.Equal("pe.Id <> 5", SqlFilter.From(m => m.Id).NotEqualTo(5).RawSql); 56 | Assert.Equal("pe.Name <> pe.LastName", SqlFilter.From(m => m.Name).NotEqualTo(m => m.LastName).RawSql); 57 | Assert.Equal("pe.Id <> pa.PersonId", SqlFilter.From(m => m.Id).NotEqualTo(m => m.PersonId).RawSql); 58 | 59 | var peAlias = new SqlAlias("per"); 60 | var paAlias = new SqlAlias("pas"); 61 | Assert.Equal("pe.Name <> per.LastName", SqlFilter.From(m => m.Name).NotEqualTo(m => m.LastName, peAlias).RawSql); 62 | Assert.Equal("pe.Id <> pas.PersonId", SqlFilter.From(m => m.Id).NotEqualTo(m => m.PersonId, paAlias).RawSql); 63 | 64 | Assert.Equal("pe.Id <> COUNT(pa.Id)", SqlFilter.From(p => p.Id).NotEqualTo(SqlField.Count(p => p.Id)).RawSql); 65 | var alias = new SqlAlias("pasp"); 66 | Assert.Equal("pe.Id <> COUNT(pasp.Id)", SqlFilter.From(p => p.Id).NotEqualTo(SqlField.Count(alias, p => p.Id)).ToString()); 67 | } 68 | 69 | [Fact] 70 | public void GreaterThan() 71 | { 72 | Assert.Equal("pe.Id > 5", SqlFilter.From(m => m.Id).GreaterThan(5).RawSql); 73 | Assert.Equal("pe.Name > pe.LastName", SqlFilter.From(m => m.Name).GreaterThan(m => m.LastName).RawSql); 74 | Assert.Equal("pe.Id > pa.PersonId", SqlFilter.From(m => m.Id).GreaterThan(m => m.PersonId).RawSql); 75 | 76 | var peAlias = new SqlAlias("per"); 77 | var paAlias = new SqlAlias("pas"); 78 | Assert.Equal("pe.Name > per.LastName", SqlFilter.From(m => m.Name).GreaterThan(m => m.LastName, peAlias).RawSql); 79 | Assert.Equal("pe.Id > pas.PersonId", SqlFilter.From(m => m.Id).GreaterThan(m => m.PersonId, paAlias).RawSql); 80 | 81 | Assert.Equal("pe.Id > COUNT(pa.Id)", SqlFilter.From(p => p.Id).GreaterThan(SqlField.Count(p => p.Id)).RawSql); 82 | var alias = new SqlAlias("pasp"); 83 | Assert.Equal("pe.Id > COUNT(pasp.Id)", SqlFilter.From(p => p.Id).GreaterThan(SqlField.Count(alias, p => p.Id)).ToString()); 84 | } 85 | 86 | [Fact] 87 | public void GreaterThanOrEqual() 88 | { 89 | Assert.Equal("pe.Id >= 5", SqlFilter.From(m => m.Id).GreaterThanOrEqual(5).RawSql); 90 | Assert.Equal("pe.Name >= pe.LastName", SqlFilter.From(m => m.Name).GreaterThanOrEqual(m => m.LastName).RawSql); 91 | Assert.Equal("pe.Id >= pa.PersonId", SqlFilter.From(m => m.Id).GreaterThanOrEqual(m => m.PersonId).RawSql); 92 | 93 | var peAlias = new SqlAlias("per"); 94 | var paAlias = new SqlAlias("pas"); 95 | Assert.Equal("pe.Name >= per.LastName", SqlFilter.From(m => m.Name).GreaterThanOrEqual(m => m.LastName, peAlias).RawSql); 96 | Assert.Equal("pe.Id >= pas.PersonId", SqlFilter.From(m => m.Id).GreaterThanOrEqual(m => m.PersonId, paAlias).RawSql); 97 | 98 | Assert.Equal("pe.Id >= COUNT(pa.Id)", SqlFilter.From(p => p.Id).GreaterThanOrEqual(SqlField.Count(p => p.Id)).RawSql); 99 | var alias = new SqlAlias("pasp"); 100 | Assert.Equal("pe.Id >= COUNT(pasp.Id)", SqlFilter.From(p => p.Id).GreaterThanOrEqual(SqlField.Count(alias, p => p.Id)).ToString()); 101 | } 102 | 103 | [Fact] 104 | public void LessThan() 105 | { 106 | Assert.Equal("pe.Id < 5", SqlFilter.From(m => m.Id).LessThan(5).RawSql); 107 | Assert.Equal("pe.Name < pe.LastName", SqlFilter.From(m => m.Name).LessThan(m => m.LastName).RawSql); 108 | Assert.Equal("pe.Id < pa.PersonId", SqlFilter.From(m => m.Id).LessThan(m => m.PersonId).RawSql); 109 | 110 | var peAlias = new SqlAlias("per"); 111 | var paAlias = new SqlAlias("pas"); 112 | Assert.Equal("pe.Name < per.LastName", SqlFilter.From(m => m.Name).LessThan(m => m.LastName, peAlias).RawSql); 113 | Assert.Equal("pe.Id < pas.PersonId", SqlFilter.From(m => m.Id).LessThan(m => m.PersonId, paAlias).RawSql); 114 | 115 | Assert.Equal("pe.Id < COUNT(pa.Id)", SqlFilter.From(p => p.Id).LessThan(SqlField.Count(p => p.Id)).RawSql); 116 | var alias = new SqlAlias("pasp"); 117 | Assert.Equal("pe.Id < COUNT(pasp.Id)", SqlFilter.From(p => p.Id).LessThan(SqlField.Count(alias, p => p.Id)).ToString()); 118 | } 119 | 120 | [Fact] 121 | public void LessThanOrEqual() 122 | { 123 | Assert.Equal("pe.Id <= 5", SqlFilter.From(m => m.Id).LessThanOrEqual(5).RawSql); 124 | Assert.Equal("pe.Name <= pe.LastName", SqlFilter.From(m => m.Name).LessThanOrEqual(m => m.LastName).RawSql); 125 | Assert.Equal("pe.Id <= pa.PersonId", SqlFilter.From(m => m.Id).LessThanOrEqual(m => m.PersonId).RawSql); 126 | 127 | var peAlias = new SqlAlias("per"); 128 | var paAlias = new SqlAlias("pas"); 129 | Assert.Equal("pe.Name <= per.LastName", SqlFilter.From(m => m.Name).LessThanOrEqual(m => m.LastName, peAlias).RawSql); 130 | Assert.Equal("pe.Id <= pas.PersonId", SqlFilter.From(m => m.Id).LessThanOrEqual(m => m.PersonId, paAlias).RawSql); 131 | 132 | Assert.Equal("pe.Id <= COUNT(pa.Id)", SqlFilter.From(p => p.Id).LessThanOrEqual(SqlField.Count(p => p.Id)).RawSql); 133 | var alias = new SqlAlias("pasp"); 134 | Assert.Equal("pe.Id <= COUNT(pasp.Id)", SqlFilter.From(p => p.Id).LessThanOrEqual(SqlField.Count(alias, p => p.Id)).ToString()); 135 | } 136 | 137 | [Fact] 138 | public void In() 139 | { 140 | Assert.Equal("pe.Id IN (5,6)", SqlFilter.From(m => m.Id).In(5, 6).RawSql); 141 | Assert.Equal("pe.Name IN ('Sergey','Alex')", SqlFilter.From(m => m.Name).In("Sergey", "Alex").RawSql); 142 | } 143 | 144 | [Fact] 145 | public void NotIn() 146 | { 147 | Assert.Equal("pe.Id NOT IN (5,6)", SqlFilter.From(m => m.Id).NotIn(5, 6).RawSql); 148 | Assert.Equal("pe.Name NOT IN ('Sergey','Alex')", SqlFilter.From(m => m.Name).NotIn("Sergey", "Alex").RawSql); 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /LambdaSql.UnitTests/Filter/SqlFilterParameterTest.cs: -------------------------------------------------------------------------------- 1 | using LambdaSql.Filter; 2 | using LambdaSql.Filter.SqlFilterItem; 3 | using Xunit; 4 | 5 | namespace LambdaSql.UnitTests.Filter 6 | { 7 | public class SqlFilterParameterTest 8 | { 9 | [Theory] 10 | [InlineData(true, "1")] 11 | [InlineData(false, "0")] 12 | public void Create_BooleanParam_ReturnsBinaryNumber(bool? param, string expected) 13 | { 14 | var configuration = new SqlFilterConfiguration {WithoutParameters = true}; 15 | 16 | var filterParam = SqlFilterParameter.Create(configuration, param); 17 | 18 | Assert.Equal(expected, filterParam.Value); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /LambdaSql.UnitTests/Filter/SqlFilterTest.cs: -------------------------------------------------------------------------------- 1 | using LambdaSql.Field; 2 | using LambdaSql.Filter; 3 | using LambdaSql.UnitTests.Entities; 4 | using Xunit; 5 | 6 | namespace LambdaSql.UnitTests.Filter 7 | { 8 | public class SqlFilterTest 9 | { 10 | [Fact] 11 | public void From_LambdaExpression_Success() 12 | { 13 | var filter = SqlFilter.From(m => m.Id).EqualTo(5); 14 | 15 | Assert.Equal("pe.Id = 5", filter.RawSql); 16 | } 17 | 18 | [Fact] 19 | public void From_LambdaExpressionWithAlias_Success() 20 | { 21 | var filter = SqlFilter.From(m => m.Id, new SqlAlias("al")).EqualTo(5); 22 | 23 | Assert.Equal("al.Id = 5", filter.RawSql); 24 | } 25 | 26 | [Fact] 27 | public void From_GenericSqlField_Success() 28 | { 29 | var filter = SqlFilter.From(SqlField.From(m => m.Id)).EqualTo(5); 30 | 31 | Assert.Equal("pe.Id = 5", filter.RawSql); 32 | } 33 | 34 | [Fact] 35 | public void From_TypedSqlField_Success() 36 | { 37 | var sqlField = new TypedSqlField(typeof(Person), typeof(int)) 38 | { 39 | Name = nameof(Person.Id), 40 | Alias = new SqlAlias("pe") 41 | }; 42 | var filter = SqlFilter.From(sqlField).EqualTo(5); 43 | 44 | Assert.Equal("pe.Id = 5", filter.RawSql); 45 | } 46 | 47 | [Fact] 48 | public void And_LambdaExpression_Success() 49 | { 50 | var filter = SqlFilter.From(m => m.Id).EqualTo(5) 51 | .And(m => m.Name).EqualTo("Sergey"); 52 | 53 | Assert.Equal("pe.Id = 5 AND pe.Name = 'Sergey'", filter.RawSql); 54 | } 55 | 56 | [Fact] 57 | public void And_EmptyFilterWithLambdaExpression_Success() 58 | { 59 | var filter = SqlFilter.Empty 60 | .And(m => m.Name).EqualTo("Sergey"); 61 | 62 | Assert.Equal("pe.Name = 'Sergey'", filter.RawSql); 63 | } 64 | 65 | [Fact] 66 | public void And_LambdaExpressionWithAlias_Success() 67 | { 68 | var filter = SqlFilter.From(m => m.Id).EqualTo(5) 69 | .And(m => m.Name, new SqlAlias("al")).EqualTo("Sergey"); 70 | 71 | Assert.Equal("pe.Id = 5 AND al.Name = 'Sergey'", filter.RawSql); 72 | } 73 | 74 | [Fact] 75 | public void And_GenericSqlField_Success() 76 | { 77 | var filter = SqlFilter.From(m => m.Id).EqualTo(5) 78 | .And(SqlField.From(m => m.Name)).EqualTo("Sergey"); 79 | 80 | Assert.Equal("pe.Id = 5 AND pe.Name = 'Sergey'", filter.RawSql); 81 | } 82 | 83 | [Fact] 84 | public void And_EmptyFilterWithGenericSqlField_Success() 85 | { 86 | var filter = SqlFilter.Empty 87 | .And(SqlField.From(m => m.Name)).EqualTo("Sergey"); 88 | 89 | Assert.Equal("pe.Name = 'Sergey'", filter.RawSql); 90 | } 91 | 92 | [Fact] 93 | public void And_TypedSqlField_Success() 94 | { 95 | var sqlField = new TypedSqlField(typeof(Person), typeof(string)) 96 | { 97 | Name = nameof(Person.Name), 98 | Alias = new SqlAlias("pe") 99 | }; 100 | var filter = SqlFilter.From(m => m.Id).EqualTo(5) 101 | .And(sqlField).EqualTo("Sergey"); 102 | 103 | Assert.Equal("pe.Id = 5 AND pe.Name = 'Sergey'", filter.RawSql); 104 | } 105 | 106 | [Fact] 107 | public void And_EmptyFilterWithTypedSqlField_Success() 108 | { 109 | var sqlField = new TypedSqlField(typeof(Person), typeof(string)) 110 | { 111 | Name = nameof(Person.Name), 112 | Alias = new SqlAlias("pe") 113 | }; 114 | var filter = SqlFilter.Empty 115 | .And(sqlField).EqualTo("Sergey"); 116 | 117 | Assert.Equal("pe.Name = 'Sergey'", filter.RawSql); 118 | } 119 | 120 | [Fact] 121 | public void And_SqlFilter_Success() 122 | { 123 | var filter = SqlFilter.From(m => m.Id).EqualTo(5); 124 | 125 | Assert.Equal("pe.Id = 5 AND pe.Id = 5", filter.And(filter).RawSql); 126 | } 127 | 128 | [Fact] 129 | public void And_EmptyFilterWithSqlFilter_Success() 130 | { 131 | var filter = SqlFilter.From(m => m.Id).EqualTo(5); 132 | 133 | Assert.Equal("pe.Id = 5", SqlFilter.Empty.And(filter).RawSql); 134 | } 135 | 136 | [Fact] 137 | public void And_SqlFilterBase_Success() 138 | { 139 | var filter = SqlFilter.From(m => m.Id).EqualTo(5); 140 | var filter2 = SqlFilter.From(m => m.Id).EqualTo(6); 141 | 142 | Assert.Equal("pe.Id = 5 AND pa.Id = 6", filter.And(filter2).RawSql); 143 | } 144 | 145 | [Fact] 146 | public void And_EmptyFilterWithSqlFilterBase_Success() 147 | { 148 | var filter = SqlFilter.From(m => m.Id).EqualTo(6); 149 | 150 | Assert.Equal("pa.Id = 6", SqlFilter.Empty.And(filter).RawSql); 151 | } 152 | 153 | [Fact] 154 | public void Or_LambdaExpression_Success() 155 | { 156 | var filter = SqlFilter.From(m => m.Id).EqualTo(5) 157 | .Or(m => m.Name).EqualTo("Sergey"); 158 | 159 | Assert.Equal("pe.Id = 5 OR pe.Name = 'Sergey'", filter.RawSql); 160 | } 161 | 162 | [Fact] 163 | public void Or_EmptyFilterWithLambdaExpression_Success() 164 | { 165 | var filter = SqlFilter.Empty 166 | .Or(m => m.Name).EqualTo("Sergey"); 167 | 168 | Assert.Equal("pe.Name = 'Sergey'", filter.RawSql); 169 | } 170 | 171 | [Fact] 172 | public void Or_LambdaExpressionWithAlias_Success() 173 | { 174 | var filter = SqlFilter.From(m => m.Id).EqualTo(5) 175 | .Or(m => m.Name, new SqlAlias("al")).EqualTo("Sergey"); 176 | 177 | Assert.Equal("pe.Id = 5 OR al.Name = 'Sergey'", filter.RawSql); 178 | } 179 | 180 | [Fact] 181 | public void Or_GenericSqlField_Success() 182 | { 183 | var filter = SqlFilter.From(m => m.Id).EqualTo(5) 184 | .Or(SqlField.From(m => m.Name)).EqualTo("Sergey"); 185 | 186 | Assert.Equal("pe.Id = 5 OR pe.Name = 'Sergey'", filter.RawSql); 187 | } 188 | 189 | [Fact] 190 | public void Or_EmptyFilterWithGenericSqlField_Success() 191 | { 192 | var filter = SqlFilter.Empty 193 | .Or(SqlField.From(m => m.Name)).EqualTo("Sergey"); 194 | 195 | Assert.Equal("pe.Name = 'Sergey'", filter.RawSql); 196 | } 197 | 198 | [Fact] 199 | public void Or_TypedSqlField_Success() 200 | { 201 | var sqlField = new TypedSqlField(typeof(Person), typeof(string)) 202 | { 203 | Name = nameof(Person.Name), 204 | Alias = new SqlAlias("pe") 205 | }; 206 | var filter = SqlFilter.From(m => m.Id).EqualTo(5) 207 | .Or(sqlField).EqualTo("Sergey"); 208 | 209 | Assert.Equal("pe.Id = 5 OR pe.Name = 'Sergey'", filter.RawSql); 210 | } 211 | 212 | [Fact] 213 | public void Or_EmptyFilterWithTypedSqlField_Success() 214 | { 215 | var sqlField = new TypedSqlField(typeof(Person), typeof(string)) 216 | { 217 | Name = nameof(Person.Name), 218 | Alias = new SqlAlias("pe") 219 | }; 220 | var filter = SqlFilter.Empty 221 | .Or(sqlField).EqualTo("Sergey"); 222 | 223 | Assert.Equal("pe.Name = 'Sergey'", filter.RawSql); 224 | } 225 | 226 | [Fact] 227 | public void Or_SqlFilter_Success() 228 | { 229 | var filter = SqlFilter.From(m => m.Id).EqualTo(5); 230 | 231 | Assert.Equal("pe.Id = 5 OR pe.Id = 5", filter.Or(filter).RawSql); 232 | } 233 | 234 | [Fact] 235 | public void Or_EmptyFilterWithSqlFilter_Success() 236 | { 237 | var filter = SqlFilter.From(m => m.Id).EqualTo(5); 238 | 239 | Assert.Equal("pe.Id = 5", SqlFilter.Empty.Or(filter).RawSql); 240 | } 241 | 242 | [Fact] 243 | public void Or_SqlFilterBase_Success() 244 | { 245 | var filter = SqlFilter.From(m => m.Id).EqualTo(5); 246 | var filter2 = SqlFilter.From(m => m.Id).EqualTo(6); 247 | 248 | Assert.Equal("pe.Id = 5 OR pa.Id = 6", filter.Or(filter2).RawSql); 249 | } 250 | 251 | [Fact] 252 | public void Or_EmptyFilterWithSqlFilterBase_Success() 253 | { 254 | var filter = SqlFilter.From(m => m.Id).EqualTo(6); 255 | 256 | Assert.Equal("pa.Id = 6", SqlFilter.Empty.Or(filter).RawSql); 257 | } 258 | 259 | [Fact] 260 | public void AndGroup_SqlFilter_Success() 261 | { 262 | var filter = SqlFilter.From(m => m.Id).EqualTo(5); 263 | 264 | Assert.Equal("pe.Id = 5 AND (pe.Id = 5)", filter.AndGroup(filter).RawSql); 265 | } 266 | 267 | [Fact] 268 | public void AndGroup_EmptyFilterWithSqlFilter_Success() 269 | { 270 | var filter = SqlFilter.From(m => m.Id).EqualTo(5); 271 | 272 | Assert.Equal("(pe.Id = 5)", SqlFilter.Empty.AndGroup(filter).RawSql); 273 | } 274 | 275 | [Fact] 276 | public void AndGroup_SqlFilterBase_Success() 277 | { 278 | var filter = SqlFilter.From(m => m.Id).EqualTo(5); 279 | var filter2 = SqlFilter.From(m => m.Id).EqualTo(6); 280 | 281 | Assert.Equal("pe.Id = 5 AND (pa.Id = 6)", filter.AndGroup(filter2).RawSql); 282 | } 283 | 284 | [Fact] 285 | public void AndGroup_EmptyFilterWithSqlFilterBase_Success() 286 | { 287 | var filter = SqlFilter.From(m => m.Id).EqualTo(6); 288 | 289 | Assert.Equal("(pa.Id = 6)", SqlFilter.Empty.AndGroup(filter).RawSql); 290 | } 291 | 292 | [Fact] 293 | public void OrGroup_SqlFilter_Success() 294 | { 295 | var filter = SqlFilter.From(m => m.Id).EqualTo(5); 296 | 297 | Assert.Equal("pe.Id = 5 OR (pe.Id = 5)", filter.OrGroup(filter).RawSql); 298 | } 299 | 300 | [Fact] 301 | public void OrGroup_EmptyFilterWithSqlFilter_Success() 302 | { 303 | var filter = SqlFilter.From(m => m.Id).EqualTo(5); 304 | 305 | Assert.Equal("(pe.Id = 5)", SqlFilter.Empty.OrGroup(filter).RawSql); 306 | } 307 | 308 | [Fact] 309 | public void OrGroup_SqlFilterBase_Success() 310 | { 311 | var filter = SqlFilter.From(m => m.Id).EqualTo(5); 312 | var filter2 = SqlFilter.From(m => m.Id).EqualTo(6); 313 | 314 | Assert.Equal("pe.Id = 5 OR (pa.Id = 6)", filter.OrGroup(filter2).RawSql); 315 | } 316 | 317 | [Fact] 318 | public void OrGroup_EmptyFilterWithSqlFilterBase_Success() 319 | { 320 | var filter = SqlFilter.From(m => m.Id).EqualTo(6); 321 | 322 | Assert.Equal("(pa.Id = 6)", SqlFilter.Empty.OrGroup(filter).RawSql); 323 | } 324 | 325 | //--------------------------------------------------------------------------------------------------- 326 | 327 | [Fact] 328 | public void And_Lambda_InstanceIsImmutable() 329 | { 330 | var filter = SqlFilter.From(m => m.Id).EqualTo(5); 331 | 332 | Assert.Equal("pe.Id = 5 AND pe.Name = 'Sergey'", filter.And(m => m.Name).EqualTo("Sergey").RawSql); 333 | Assert.Equal("pe.Id = 5", filter.RawSql); 334 | } 335 | 336 | [Fact] 337 | public void And_WithoutAliases_Success() 338 | { 339 | var filter = SqlFilter.From(m => m.Id).EqualTo(5) 340 | .And(m => m.Name).EqualTo("Sergey"); 341 | 342 | Assert.Equal("Id = 5 AND Name = 'Sergey'", filter.WithoutAliases().RawSql); 343 | Assert.Equal("pe.Id = 5 AND pe.Name = 'Sergey'", filter.RawSql); 344 | } 345 | 346 | [Fact] 347 | public void And_WithAliases_Success() 348 | { 349 | var filter = SqlFilter.From(m => m.Id).EqualTo(5) 350 | .And(m => m.Name).EqualTo("Sergey"); 351 | var filterWithoutAliases = filter.WithoutAliases(); 352 | 353 | Assert.Equal("Id = 5 AND Name = 'Sergey'", filterWithoutAliases.RawSql); 354 | Assert.Equal("pe.Id = 5 AND pe.Name = 'Sergey'", filter.RawSql); 355 | Assert.Equal("pe.Id = 5 AND pe.Name = 'Sergey'", filterWithoutAliases.WithAliases().RawSql); 356 | } 357 | 358 | [Fact] 359 | public void And_WithParameterPrefix_Success() 360 | { 361 | var filter = SqlFilter.From(m => m.Id).EqualTo(5); 362 | 363 | Assert.Equal("pe.Id = @p0", filter.ParametricSql); 364 | Assert.Equal("pe.Id = @prm0", filter.WithParameterPrefix("prm").ParametricSql); 365 | } 366 | 367 | [Fact] 368 | public void AsMultitable_SqlFilter_Converted() 369 | { 370 | var filter = SqlFilter.From(m => m.Id).EqualTo(5); 371 | 372 | var multitableFilter = filter.AsMultitable(); 373 | 374 | Assert.Equal("pe.Id = 5", filter.RawSql); 375 | Assert.Equal("pe.Id = 5", multitableFilter.RawSql); 376 | Assert.Equal("pe.Id = 5 AND pa.Id = 3", multitableFilter 377 | .And(SqlFilter.From(p => p.Id).EqualTo(3)).RawSql); 378 | } 379 | } 380 | } 381 | -------------------------------------------------------------------------------- /LambdaSql.UnitTests/LambdaSql.UnitTests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net48 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | all 13 | runtime; build; native; contentfiles; analyzers; buildtransitive 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /LambdaSql.UnitTests/MetadataProviderTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Data; 3 | using System.Diagnostics.CodeAnalysis; 4 | using System.Linq.Expressions; 5 | using Xunit; 6 | 7 | namespace LambdaSql.UnitTests 8 | { 9 | public class MetadataProviderTest 10 | { 11 | // ReSharper disable ClassNeverInstantiated.Local 12 | [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local")] 13 | private class TestEntity 14 | { 15 | public int Id { get; set; } 16 | public string Name { get; set; } 17 | } 18 | 19 | private class TestEntity2 : TestEntity 20 | { 21 | } 22 | 23 | private class TestEntity3 : TestEntity 24 | { 25 | } 26 | // ReSharper restore ClassNeverInstantiated.Local 27 | 28 | [Fact] 29 | public void TableNameIsValid() 30 | { 31 | Assert.Equal("TestEntity", MetadataProvider.Instance.GetTableName()); 32 | Assert.Equal("TestEntity2", MetadataProvider.Instance.GetTableName()); 33 | Assert.Equal("TestEntity3", MetadataProvider.Instance.GetTableName()); 34 | } 35 | 36 | [Fact] 37 | public void PropertyNameIsValid() 38 | { 39 | Expression> test1 = entity => entity.Name; 40 | Expression> test2 = entity => entity.Id; 41 | Expression> test3 = entity => entity.Id; 42 | 43 | Assert.Equal("Name", MetadataProvider.Instance.GetPropertyName(test1)); 44 | Assert.Equal("Id", MetadataProvider.Instance.GetPropertyName(test2)); 45 | Assert.Equal("Id", MetadataProvider.Instance.GetPropertyName(test3)); 46 | } 47 | 48 | [Theory] 49 | [InlineData("5", "'5'")] 50 | [InlineData(true, "1")] 51 | [InlineData(false, "0")] 52 | [InlineData(5, "5")] 53 | public void ParameterToString_SomeValueWithoutDbType_ConvertedCorrectly(object value, string expectedString) 54 | { 55 | Assert.Equal(expectedString, MetadataProvider.Instance.ParameterToString(value)); 56 | } 57 | 58 | [Theory] 59 | [InlineData("5", DbType.Int32, "5")] 60 | [InlineData(5, DbType.String, "'5'")] 61 | [InlineData(true, DbType.Boolean, "1")] 62 | [InlineData(false, DbType.Boolean, "0")] 63 | public void ParameterToString_SomeValueWithDbType_ConvertedCorrectly(object value, DbType dbType, string expectedString) 64 | { 65 | Assert.Equal(expectedString, MetadataProvider.Instance.ParameterToString(value, dbType)); 66 | } 67 | 68 | [Fact] 69 | public void ThrowExceptionsForSimilarEntities() 70 | { 71 | MetadataProvider.Instance.AliasFor(); 72 | Assert.Throws(() => MetadataProvider.Instance.AliasFor()); 73 | Assert.Throws(() => MetadataProvider.Instance.AliasFor()); 74 | MetadataProvider.Instance.AliasFor(); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /LambdaSql.UnitTests/SqlFieldTest.cs: -------------------------------------------------------------------------------- 1 | using LambdaSql.Field; 2 | using LambdaSql.UnitTests.Entities; 3 | using Xunit; 4 | 5 | namespace LambdaSql.UnitTests 6 | { 7 | public class SqlFieldTest 8 | { 9 | [Fact] 10 | public void From_StringViewIsValid() 11 | { 12 | Assert.Equal("pe.Name AS TestAlias", SqlField.From(p => p.Name, "TestAlias").ToString()); 13 | var alias = new SqlAlias("t"); 14 | Assert.Equal("t.Number AS Passp", SqlField.From(alias, p => p.Number, "Passp").ToString()); 15 | } 16 | 17 | [Fact] 18 | public void Min_StringViewIsValid() 19 | { 20 | Assert.Equal("MIN(pe.Name) AS TestAlias", SqlField.Min(p => p.Name, "TestAlias").ToString()); 21 | var alias = new SqlAlias("t"); 22 | Assert.Equal("MIN(t.Number) AS Passp", SqlField.Min(alias, p => p.Number, "Passp").ToString()); 23 | } 24 | 25 | [Fact] 26 | public void Max_StringViewIsValid() 27 | { 28 | Assert.Equal("MAX(pe.Name) AS TestAlias", SqlField.Max(p => p.Name, "TestAlias").ToString()); 29 | var alias = new SqlAlias("t"); 30 | Assert.Equal("MAX(t.Number) AS Passp", SqlField.Max(alias, p => p.Number, "Passp").ToString()); 31 | } 32 | 33 | [Fact] 34 | public void Avg_StringViewIsValid() 35 | { 36 | Assert.Equal("AVG(pe.Name) AS TestAlias", SqlField.Avg(p => p.Name, "TestAlias").ToString()); 37 | var alias = new SqlAlias("t"); 38 | Assert.Equal("AVG(t.Number) AS Passp", SqlField.Avg(alias, p => p.Number, "Passp").ToString()); 39 | } 40 | 41 | [Fact] 42 | public void Sum_StringViewIsValid() 43 | { 44 | Assert.Equal("SUM(pe.Name) AS TestAlias", SqlField.Sum(p => p.Name, "TestAlias").ToString()); 45 | var alias = new SqlAlias("t"); 46 | Assert.Equal("SUM(t.Number) AS Passp", SqlField.Sum(alias, p => p.Number, "Passp").ToString()); 47 | } 48 | 49 | [Fact] 50 | public void Count_StringViewIsValid() 51 | { 52 | Assert.Equal("COUNT(pe.Name) AS TestAlias", SqlField.Count(p => p.Name, "TestAlias").ToString()); 53 | var alias = new SqlAlias("t"); 54 | Assert.Equal("COUNT(t.Number) AS Passp", SqlField.Count(alias, p => p.Number, "Passp").ToString()); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /LambdaSql.UnitTests/SqlJoinTest.cs: -------------------------------------------------------------------------------- 1 | using LambdaSql.Filter; 2 | using LambdaSql.UnitTests.Entities; 3 | using Xunit; 4 | 5 | namespace LambdaSql.UnitTests 6 | { 7 | public class SqlJoinTest 8 | { 9 | [Fact] 10 | public void InnerJoin() 11 | { 12 | var alias = new SqlAlias("pas"); 13 | var expected = 14 | @"INNER JOIN 15 | Passport pas ON pas.PersonId = pe.Id"; 16 | Assert.Equal(expected, new SqlJoin(JoinType.Inner, 17 | SqlFilter.From(p => p.PersonId, alias).EqualTo(p => p.Id), alias).ToString()); 18 | } 19 | 20 | [Fact] 21 | public void InnerJoinWithLongFilter() 22 | { 23 | var alias = new SqlAlias("pas"); 24 | var expected = 25 | @"INNER JOIN 26 | Passport pas ON pas.PersonId = pe.Id AND pa.Number IS NOT NULL"; 27 | Assert.Equal(expected, 28 | new SqlJoin(JoinType.Inner, 29 | SqlFilter.From(p => p.PersonId, alias).EqualTo(p => p.Id) 30 | .And(p => p.Number).IsNotNull(), alias).ToString()); 31 | } 32 | 33 | [Fact] 34 | public void LeftJoin() 35 | { 36 | var alias = new SqlAlias("pas"); 37 | var expected = 38 | @"LEFT JOIN 39 | Passport pas ON pe.Id = pas.PersonId"; 40 | Assert.Equal(expected, new SqlJoin(JoinType.Left, 41 | SqlFilter.From(p => p.Id).EqualTo(p => p.PersonId, alias), alias).ToString()); 42 | } 43 | 44 | [Fact] 45 | public void RightJoin() 46 | { 47 | var alias = new SqlAlias("pas"); 48 | var expected = 49 | @"RIGHT JOIN 50 | Passport pas ON pe.Id = pas.PersonId"; 51 | Assert.Equal(expected, new SqlJoin(JoinType.Right, 52 | SqlFilter.From(p => p.Id).EqualTo(p => p.PersonId, alias), alias).ToString()); 53 | } 54 | 55 | [Fact] 56 | public void FullJoin() 57 | { 58 | var alias = new SqlAlias("pas"); 59 | var expected = 60 | @"FULL JOIN 61 | Passport pas ON pe.Id = pas.PersonId"; 62 | Assert.Equal(expected, new SqlJoin(JoinType.Full, 63 | SqlFilter.From(p => p.Id).EqualTo(p => p.PersonId, alias), alias).ToString()); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /LambdaSql.UnitTests/SqlSelectTest.cs: -------------------------------------------------------------------------------- 1 | using LambdaSql.Field; 2 | using LambdaSql.Filter; 3 | using LambdaSql.UnitTests.Entities; 4 | using Xunit; 5 | 6 | namespace LambdaSql.UnitTests 7 | { 8 | public class SqlSelectTest 9 | { 10 | [Fact] 11 | public void IncorrectAliasThrowsException() 12 | { 13 | var select = new SqlSelect() 14 | .InnerJoin((person, passport) => person.Id == passport.PersonId) 15 | .AddFields(SqlField.Count(p => p.Id, "pa")); 16 | Assert.Throws(() => { var cmd = select.ParametricSql; }); 17 | } 18 | 19 | [Fact] 20 | public void SimpleSelect() 21 | { 22 | var select = new SqlSelect(); 23 | 24 | var expected = 25 | @"SELECT 26 | * 27 | FROM 28 | Person pe"; 29 | Assert.Equal(expected, select.ParametricSql); 30 | } 31 | 32 | [Fact] 33 | public void DistinctAndTop() 34 | { 35 | var select = new SqlSelect() 36 | .AddFields(p => p.Id) 37 | .Distinct() 38 | .Top(5); 39 | 40 | var expected = 41 | @"SELECT 42 | TOP 5 DISTINCT pe.Id 43 | FROM 44 | Person pe"; 45 | Assert.Equal(expected, select.ParametricSql); 46 | } 47 | 48 | [Fact] 49 | public void Where() 50 | { 51 | var select = new SqlSelect() 52 | .AddFields(p => p.Id) 53 | .Where(SqlFilter.From(p => p.LastName).IsNotNull()); 54 | 55 | var expected = 56 | @"SELECT 57 | pe.Id 58 | FROM 59 | Person pe 60 | WHERE 61 | pe.LastName IS NOT NULL"; 62 | Assert.Equal(expected, select.ParametricSql); 63 | } 64 | 65 | [Fact] 66 | public void GroupBy() 67 | { 68 | var select = new SqlSelect() 69 | .AddFields(SqlField.Count(p => p.Id)) 70 | .GroupBy(p => p.LastName); 71 | 72 | var expected = 73 | @"SELECT 74 | COUNT(pe.Id) 75 | FROM 76 | Person pe 77 | GROUP BY 78 | pe.LastName"; 79 | Assert.Equal(expected, select.ParametricSql); 80 | } 81 | 82 | [Fact] 83 | public void Having() 84 | { 85 | var select = new SqlSelect() 86 | .AddFields(SqlField.Count(p => p.Id)) 87 | .GroupBy(p => p.LastName) 88 | .Having(SqlFilter.From(SqlField.Count(p => p.Id)).GreaterThan(2)); 89 | 90 | var expected = 91 | @"SELECT 92 | COUNT(pe.Id) 93 | FROM 94 | Person pe 95 | GROUP BY 96 | pe.LastName 97 | HAVING 98 | COUNT(pe.Id) > 2"; 99 | Assert.Equal(expected, select.RawSql); 100 | } 101 | 102 | [Fact] 103 | public void OrderBy() 104 | { 105 | var select = new SqlSelect() 106 | .Where(SqlFilter.From(p => p.LastName).IsNotNull()) 107 | .OrderBy(p => p.Id); 108 | 109 | var expected = 110 | @"SELECT 111 | * 112 | FROM 113 | Person pe 114 | WHERE 115 | pe.LastName IS NOT NULL 116 | ORDER BY 117 | pe.Id"; 118 | Assert.Equal(expected, select.ParametricSql); 119 | } 120 | 121 | [Fact] 122 | public void Joins() 123 | { 124 | var joinByLambdaQry = new SqlSelect() 125 | .InnerJoin((person, passport) => person.Id == passport.PersonId); 126 | var joinByFilterQry = new SqlSelect() 127 | .InnerJoin(SqlFilter.From(p => p.Id).EqualTo(p => p.PersonId)); 128 | 129 | var expected = 130 | @"SELECT 131 | * 132 | FROM 133 | Person pe 134 | INNER JOIN 135 | Passport pa ON pe.Id = pa.PersonId"; 136 | Assert.Equal(expected, joinByLambdaQry.ParametricSql); 137 | Assert.Equal(expected, joinByFilterQry.ParametricSql); 138 | } 139 | 140 | [Fact] 141 | public void LambdaJoinWithInvertOrder() 142 | { 143 | var joinByLambdaQry = new SqlSelect() 144 | .InnerJoin((person, passport) => passport.PersonId == person.Id); 145 | 146 | var expected = 147 | @"SELECT 148 | * 149 | FROM 150 | Person pe 151 | INNER JOIN 152 | Passport pa ON pa.PersonId = pe.Id"; 153 | Assert.Equal(expected, joinByLambdaQry.ParametricSql); 154 | } 155 | 156 | [Fact] 157 | public void LambdaJoinWithNullableType() 158 | { 159 | var joinByLambdaQry = new SqlSelect() 160 | .InnerJoin((person, passport) => person.Id == passport.NullablePersonId); 161 | 162 | var expected = 163 | @"SELECT 164 | * 165 | FROM 166 | Person pe 167 | INNER JOIN 168 | Passport pa ON pe.Id = pa.NullablePersonId"; 169 | Assert.Equal(expected, joinByLambdaQry.ParametricSql); 170 | } 171 | 172 | [Fact] 173 | public void TotalTest() 174 | { 175 | var countFld = SqlField.Count(p => p.LastName); 176 | var select = new SqlSelect() 177 | .AddFields(p => p.LastName, p => p.Name) 178 | .AddFields(p => p.Number) 179 | .AddFields(countFld) 180 | .InnerJoin((person, passport) => person.Id == passport.PersonId) 181 | .Where(SqlFilter.From(p => p.Number).IsNotNull().And(p => p.Number).NotEqualTo("3812-808316")) 182 | .GroupBy(p => p.LastName) 183 | .Having(SqlFilter.From(countFld).GreaterThan(2)) 184 | .OrderBy(p => p.LastName); 185 | 186 | var expected = 187 | @"SELECT 188 | pe.LastName, pe.Name, pa.Number, COUNT(pe.LastName) 189 | FROM 190 | Person pe 191 | INNER JOIN 192 | Passport pa ON pe.Id = pa.PersonId 193 | WHERE 194 | pa.Number IS NOT NULL AND pa.Number <> '3812-808316' 195 | GROUP BY 196 | pe.LastName 197 | HAVING 198 | COUNT(pe.LastName) > 2 199 | ORDER BY 200 | pe.LastName"; 201 | Assert.Equal(expected, select.RawSql); 202 | } 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /LambdaSql.UnitTests/SqlSelectWrapperTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using LambdaSql.UnitTests.Entities; 3 | using Xunit; 4 | 5 | namespace LambdaSql.UnitTests 6 | { 7 | public class SqlSelectWrapperTest 8 | { 9 | [Fact] 10 | public void SimpleNestedSelect() 11 | { 12 | var select = new SqlSelect(new SqlSelect(), new SqlAlias("p")); 13 | var expected = 14 | @"SELECT 15 | * 16 | FROM 17 | ( 18 | SELECT 19 | * 20 | FROM 21 | Person pe 22 | ) AS p"; 23 | Assert.Equal(expected, select.ParametricSql); 24 | } 25 | 26 | [Fact] 27 | public void MultiNestedSelect() 28 | { 29 | var select = new SqlSelect(new SqlSelect(new SqlSelect(), new SqlAlias("p0")), new SqlAlias("p1")); 30 | var expected = 31 | @"SELECT 32 | * 33 | FROM 34 | ( 35 | SELECT 36 | * 37 | FROM 38 | ( 39 | SELECT 40 | * 41 | FROM 42 | Person pe 43 | ) AS p0 44 | ) AS p1"; 45 | Assert.Equal(expected, select.ParametricSql); 46 | } 47 | 48 | [Fact] 49 | public void IncorrectSelectedFieldsThrowsException() 50 | { 51 | var innerQry = new SqlSelect() 52 | .AddFields(p => p.LastName); 53 | var incorrectQry = new SqlSelect(innerQry, new SqlAlias("p")) 54 | .AddFields(p => p.Name); 55 | Assert.Throws(() => { var cmd = incorrectQry.ParametricSql; }); 56 | 57 | var correctQry = new SqlSelect(innerQry, new SqlAlias("p")) 58 | .AddFields(p => p.LastName); 59 | var expected = 60 | @"SELECT 61 | p.LastName 62 | FROM 63 | ( 64 | SELECT 65 | pe.LastName 66 | FROM 67 | Person pe 68 | ) AS p"; 69 | Assert.Equal(expected, correctQry.ParametricSql); 70 | } 71 | 72 | [Fact] 73 | public void IncorrectSelectedFieldsWithNestedQueryThrowsException() 74 | { 75 | var innerQry = new SqlSelect 76 | ( 77 | new SqlSelect() 78 | .AddFields(p => p.LastName), 79 | new SqlAlias("p0") 80 | ).AddFields(p => p.LastName); 81 | var incorrectQry = new SqlSelect(innerQry, new SqlAlias("p1")) 82 | .AddFields(p => p.Name); 83 | Assert.Throws(() => { var cmd = incorrectQry.ParametricSql; }); 84 | 85 | var correctQry = new SqlSelect(innerQry, new SqlAlias("p1")) 86 | .AddFields(p => p.LastName); 87 | var expected = 88 | @"SELECT 89 | p1.LastName 90 | FROM 91 | ( 92 | SELECT 93 | p0.LastName 94 | FROM 95 | ( 96 | SELECT 97 | pe.LastName 98 | FROM 99 | Person pe 100 | ) AS p0 101 | ) AS p1"; 102 | Assert.Equal(expected, correctQry.ParametricSql); 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /LambdaSql.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26430.6 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LambdaSql", "LambdaSql\LambdaSql.csproj", "{2515E663-2F44-44A3-848D-4F3A88669014}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LambdaSql.UnitTests", "LambdaSql.UnitTests\LambdaSql.UnitTests.csproj", "{D0A04172-7C51-487E-93F8-D640A4FAFE1C}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LambdaSql.Analyzers", "LambdaSql.Analyzers\LambdaSql.Analyzers.csproj", "{FDA275CB-C3DE-4B68-BF23-72E3E7A73DBF}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LambdaSql.Analyzers.Test", "LambdaSql.Analyzers.Tests\LambdaSql.Analyzers.Tests.csproj", "{565E194E-A3E1-4F1B-AFD5-E647B13875A5}" 13 | EndProject 14 | Global 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|Any CPU = Debug|Any CPU 17 | Release|Any CPU = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 20 | {2515E663-2F44-44A3-848D-4F3A88669014}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {2515E663-2F44-44A3-848D-4F3A88669014}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {2515E663-2F44-44A3-848D-4F3A88669014}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {2515E663-2F44-44A3-848D-4F3A88669014}.Release|Any CPU.Build.0 = Release|Any CPU 24 | {D0A04172-7C51-487E-93F8-D640A4FAFE1C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {D0A04172-7C51-487E-93F8-D640A4FAFE1C}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {D0A04172-7C51-487E-93F8-D640A4FAFE1C}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {D0A04172-7C51-487E-93F8-D640A4FAFE1C}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {FDA275CB-C3DE-4B68-BF23-72E3E7A73DBF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {FDA275CB-C3DE-4B68-BF23-72E3E7A73DBF}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {FDA275CB-C3DE-4B68-BF23-72E3E7A73DBF}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {FDA275CB-C3DE-4B68-BF23-72E3E7A73DBF}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {565E194E-A3E1-4F1B-AFD5-E647B13875A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {565E194E-A3E1-4F1B-AFD5-E647B13875A5}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {565E194E-A3E1-4F1B-AFD5-E647B13875A5}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {565E194E-A3E1-4F1B-AFD5-E647B13875A5}.Release|Any CPU.Build.0 = Release|Any CPU 36 | {831D2CA8-6D8E-4C1E-A095-13CB312D46B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 37 | {831D2CA8-6D8E-4C1E-A095-13CB312D46B7}.Debug|Any CPU.Build.0 = Debug|Any CPU 38 | {831D2CA8-6D8E-4C1E-A095-13CB312D46B7}.Release|Any CPU.ActiveCfg = Release|Any CPU 39 | {831D2CA8-6D8E-4C1E-A095-13CB312D46B7}.Release|Any CPU.Build.0 = Release|Any CPU 40 | EndGlobalSection 41 | GlobalSection(SolutionProperties) = preSolution 42 | HideSolutionNode = FALSE 43 | EndGlobalSection 44 | GlobalSection(ExtensibilityGlobals) = postSolution 45 | SolutionGuid = {CF36A01D-EEF3-47D1-8636-F530661F6A32} 46 | EndGlobalSection 47 | EndGlobal 48 | -------------------------------------------------------------------------------- /LambdaSql/Exceptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace LambdaSql 4 | { 5 | public class JoinException : Exception 6 | { 7 | public JoinException(string message) : base(message) 8 | { 9 | } 10 | } 11 | 12 | public class DuplicateAliasException : Exception 13 | { 14 | public DuplicateAliasException(string message) : base(message) 15 | { 16 | } 17 | } 18 | 19 | public class IncorrectAliasException : Exception 20 | { 21 | public IncorrectAliasException(string message) : base(message) 22 | { 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /LambdaSql/Extensions.cs: -------------------------------------------------------------------------------- 1 | namespace LambdaSql 2 | { 3 | internal static class Extensions 4 | { 5 | internal static bool IsNotEmpty(this string value) 6 | { 7 | return !string.IsNullOrEmpty(value); 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /LambdaSql/Field/AggregateFunc.cs: -------------------------------------------------------------------------------- 1 | namespace LambdaSql.Field 2 | { 3 | public enum AggregateFunc 4 | { 5 | Min, 6 | Max, 7 | Avg, 8 | Sum, 9 | Count 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /LambdaSql/Field/ISqlField.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace LambdaSql.Field 4 | { 5 | public interface ISqlField 6 | { 7 | ISqlAlias Alias { get; set; } 8 | Type EntityType { get; } 9 | string Name { get; } 10 | string AsAlias { get; set; } 11 | string ShortString { get; } 12 | string String { get; } 13 | AggregateFunc? Aggregation { get; } 14 | ISqlField Clone(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /LambdaSql/Field/ITypedSqlField.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace LambdaSql.Field 4 | { 5 | public interface ITypedSqlField : ISqlField 6 | { 7 | Type FieldType { get; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /LambdaSql/Field/SqlField.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq.Expressions; 3 | using System.Text; 4 | 5 | namespace LambdaSql.Field 6 | { 7 | public class SqlField : ISqlField 8 | { 9 | internal SqlField(Type entityType) 10 | { 11 | EntityType = entityType; 12 | } 13 | 14 | public ISqlAlias Alias { get; set; } 15 | public Type EntityType { get; } 16 | public string Name { get; internal set; } 17 | public string AsAlias { get; set; } 18 | public AggregateFunc? Aggregation { get; internal set; } 19 | 20 | public string ShortString => GetShortStringBuilder().ToString(); 21 | 22 | public string String 23 | { 24 | get 25 | { 26 | var sb = GetShortStringBuilder(); 27 | if (AsAlias != null) 28 | sb.Append(" AS ").Append(AsAlias); 29 | return sb.ToString(); 30 | } 31 | } 32 | 33 | private StringBuilder GetShortStringBuilder() 34 | { 35 | var sb = Alias != null && Alias.Value.IsNotEmpty() 36 | ? new StringBuilder(Alias.Value + "." + Name) 37 | : new StringBuilder(Name); 38 | if (Aggregation != null) 39 | sb.Insert(0, Aggregation.ToString().ToUpper() + "(").Append(")"); 40 | return sb; 41 | } 42 | 43 | public override string ToString() => String; 44 | 45 | public ISqlField Clone() => (ISqlField)MemberwiseClone(); 46 | 47 | public static SqlField From(Type entityType, ISqlAlias alias, string name, string asAlias = null) 48 | { 49 | if (name == null) throw new ArgumentNullException(nameof(name)); 50 | return new SqlField(entityType) 51 | { 52 | Alias = alias, 53 | Name = name, 54 | AsAlias = asAlias 55 | }; 56 | } 57 | 58 | public static TypedSqlField From(Type entityType, Type fieldType, ISqlAlias alias, string name, string asAlias = null) 59 | { 60 | if (name == null) throw new ArgumentNullException(nameof(name)); 61 | return new TypedSqlField(entityType, fieldType) 62 | { 63 | Alias = alias, 64 | Name = name, 65 | AsAlias = asAlias 66 | }; 67 | } 68 | } 69 | 70 | public class TypedSqlField : SqlField, ITypedSqlField 71 | { 72 | internal TypedSqlField(Type entityType, Type fieldType) : base(entityType) 73 | { 74 | FieldType = fieldType; 75 | } 76 | 77 | public Type FieldType { get; } 78 | } 79 | 80 | public class SqlField : TypedSqlField 81 | { 82 | internal SqlField() : base(typeof(TEntity), typeof(TFieldType)) 83 | { 84 | } 85 | } 86 | 87 | public class SqlField : SqlField 88 | { 89 | internal SqlField() : base(typeof(TEntity)) 90 | { 91 | } 92 | 93 | private static SqlField Aggregate(ISqlAlias alias, 94 | LambdaExpression field, AggregateFunc? aggregation, string asAlias) 95 | { 96 | if (field == null) throw new ArgumentNullException(nameof(field)); 97 | if (alias == null) throw new ArgumentNullException(nameof(alias)); 98 | var fieldName = MetadataProvider.Instance.GetPropertyName(field); 99 | return new SqlField { Alias = alias, Name = fieldName, AsAlias = asAlias, Aggregation = aggregation }; 100 | } 101 | 102 | public static SqlField From(Expression> field, string asAlias = null) 103 | { 104 | if (field == null) throw new ArgumentNullException(nameof(field)); 105 | return From(MetadataProvider.Instance.AliasFor(), field, asAlias); 106 | } 107 | 108 | public static SqlField From(SqlAlias alias, Expression> field, string asAlias = null) 109 | { 110 | if (field == null) throw new ArgumentNullException(nameof(field)); 111 | if (alias == null) throw new ArgumentNullException(nameof(alias)); 112 | return Aggregate(alias, field, null, asAlias); 113 | } 114 | 115 | public static SqlField Min(Expression> field, string asAlias = null) 116 | { 117 | if (field == null) throw new ArgumentNullException(nameof(field)); 118 | return Min(MetadataProvider.Instance.AliasFor(), field, asAlias); 119 | } 120 | 121 | public static SqlField Min(SqlAlias alias, Expression> field, string asAlias = null) 122 | { 123 | if (field == null) throw new ArgumentNullException(nameof(field)); 124 | if (alias == null) throw new ArgumentNullException(nameof(alias)); 125 | return Aggregate(alias, field, AggregateFunc.Min, asAlias); 126 | } 127 | 128 | public static SqlField Max(Expression> field, string asAlias = null) 129 | { 130 | if (field == null) throw new ArgumentNullException(nameof(field)); 131 | return Max(MetadataProvider.Instance.AliasFor(), field, asAlias); 132 | } 133 | 134 | public static SqlField Max(SqlAlias alias, Expression> field, string asAlias = null) 135 | { 136 | if (field == null) throw new ArgumentNullException(nameof(field)); 137 | if (alias == null) throw new ArgumentNullException(nameof(alias)); 138 | return Aggregate(alias, field, AggregateFunc.Max, asAlias); 139 | } 140 | 141 | public static SqlField Avg(Expression> field, string asAlias = null) 142 | { 143 | if (field == null) throw new ArgumentNullException(nameof(field)); 144 | return Avg(MetadataProvider.Instance.AliasFor(), field, asAlias); 145 | } 146 | 147 | public static SqlField Avg(SqlAlias alias, Expression> field, string asAlias = null) 148 | { 149 | if (field == null) throw new ArgumentNullException(nameof(field)); 150 | if (alias == null) throw new ArgumentNullException(nameof(alias)); 151 | return Aggregate(alias, field, AggregateFunc.Avg, asAlias); 152 | } 153 | 154 | public static SqlField Sum(Expression> field, string asAlias = null) 155 | { 156 | if (field == null) throw new ArgumentNullException(nameof(field)); 157 | return Sum(MetadataProvider.Instance.AliasFor(), field, asAlias); 158 | } 159 | 160 | public static SqlField Sum(SqlAlias alias, Expression> field, string asAlias = null) 161 | { 162 | if (field == null) throw new ArgumentNullException(nameof(field)); 163 | if (alias == null) throw new ArgumentNullException(nameof(alias)); 164 | return Aggregate(alias, field, AggregateFunc.Sum, asAlias); 165 | } 166 | 167 | public static SqlField Count(Expression> field, string asAlias = null) 168 | { 169 | if (field == null) throw new ArgumentNullException(nameof(field)); 170 | return Count(MetadataProvider.Instance.AliasFor(), field, asAlias); 171 | } 172 | 173 | public static SqlField Count(SqlAlias alias, Expression> field, string asAlias = null) 174 | { 175 | if (field == null) throw new ArgumentNullException(nameof(field)); 176 | if (alias == null) throw new ArgumentNullException(nameof(alias)); 177 | return Aggregate(alias, field, AggregateFunc.Count, asAlias); 178 | } 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /LambdaSql/Filter/ISqlFilter.cs: -------------------------------------------------------------------------------- 1 | using System.Data.Common; 2 | 3 | namespace LambdaSql.Filter 4 | { 5 | public interface ISqlFilter 6 | { 7 | string RawSql { get; } 8 | string ParametricSql { get; } 9 | DbParameter[] Parameters { get; } 10 | ISqlFilter WithParameterPrefix(string prefix); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /LambdaSql/Filter/MultitableSqlFilter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Immutable; 3 | using System.Linq.Expressions; 4 | using LambdaSql.Field; 5 | using LambdaSql.Filter.SqlFilterItem; 6 | 7 | namespace LambdaSql.Filter 8 | { 9 | public class MultitableSqlFilter : SqlFilterBase 10 | { 11 | internal MultitableSqlFilter(ImmutableList sqlFilterItems) : base(sqlFilterItems) 12 | { 13 | } 14 | 15 | private static SqlFilterField> CreateField( 16 | ImmutableList items, LambdaExpression field, SqlAlias alias) 17 | { 18 | return CreateField(items, BuildSqlField(field, alias)); 19 | } 20 | 21 | private static SqlFilterField> CreateField( 22 | ImmutableList items, ITypedSqlField field) 23 | { 24 | return new SqlFilterField>(items, field, i => new MultitableSqlFilter(i)); 25 | } 26 | 27 | //----------------------------------------------------------------------------------------------------- 28 | 29 | public SqlFilterField> And(Expression> field, SqlAlias alias = null) 30 | { 31 | return CreateField(AddItem(SqlFilterItems.And), field, alias); 32 | } 33 | 34 | public SqlFilterField> And(SqlField field) 35 | { 36 | return CreateField(AddItem(SqlFilterItems.And), field); 37 | } 38 | 39 | public SqlFilterField> And(ITypedSqlField field) 40 | { 41 | CheckField(field); 42 | return CreateField(AddItem(SqlFilterItems.And), field); 43 | } 44 | 45 | public SqlFilterField> And(ITypedSqlField field) 46 | { 47 | return And(field); 48 | } 49 | 50 | public MultitableSqlFilter And(SqlFilterBase filter) 51 | { 52 | return new MultitableSqlFilter(AddItem(SqlFilterItems.And).AddRange(filter.FilterItems)); 53 | } 54 | 55 | //----------------------------------------------------------------------------------------------------- 56 | 57 | public SqlFilterField> Or(Expression> field, SqlAlias alias = null) 58 | { 59 | return CreateField(AddItem(SqlFilterItems.Or), field, alias); 60 | } 61 | 62 | public SqlFilterField> Or(SqlField field) 63 | { 64 | return CreateField(AddItem(SqlFilterItems.Or), field); 65 | } 66 | 67 | public SqlFilterField> Or(ITypedSqlField field) 68 | { 69 | CheckField(field); 70 | return CreateField(AddItem(SqlFilterItems.Or), field); 71 | } 72 | 73 | public SqlFilterField> Or(ITypedSqlField field) 74 | { 75 | return Or(field); 76 | } 77 | 78 | public MultitableSqlFilter Or(SqlFilterBase filter) 79 | { 80 | return new MultitableSqlFilter(AddItem(SqlFilterItems.Or).AddRange(filter.FilterItems)); 81 | } 82 | 83 | //----------------------------------------------------------------------------------------------------- 84 | 85 | public MultitableSqlFilter AndGroup(SqlFilterBase filter) 86 | { 87 | var items = AddItem(SqlFilterItems.And) 88 | .Add(SqlFilterItems.Build("(")) 89 | .AddRange(filter.FilterItems) 90 | .Add(SqlFilterItems.Build(")")); 91 | return new MultitableSqlFilter(items); 92 | } 93 | 94 | public MultitableSqlFilter OrGroup(SqlFilterBase filter) 95 | { 96 | var items = AddItem(SqlFilterItems.Or) 97 | .Add(SqlFilterItems.Build("(")) 98 | .AddRange(filter.FilterItems) 99 | .Add(SqlFilterItems.Build(")")); 100 | return new MultitableSqlFilter(items); 101 | } 102 | 103 | //----------------------------------------------------------------------------------------------------- 104 | 105 | public MultitableSqlFilter WithParameterPrefix(string prefix) 106 | { 107 | var filter = new MultitableSqlFilter(FilterItems); 108 | filter.ParamPrefix = prefix; 109 | return filter; 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /LambdaSql/Filter/SqlFilter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Immutable; 3 | using System.Linq.Expressions; 4 | using LambdaSql.Field; 5 | using LambdaSql.Filter.SqlFilterItem; 6 | 7 | namespace LambdaSql.Filter 8 | { 9 | public class SqlFilter : SqlFilterBase 10 | { 11 | internal SqlFilter(ImmutableList sqlFilterItems) : base(sqlFilterItems) 12 | { 13 | } 14 | 15 | public static SqlFilter Empty { get; } = new SqlFilter(ImmutableList.Empty); 16 | 17 | //----------------------------------------------------------------------------------------------------- 18 | 19 | private static SqlFilterField> CreateField( 20 | ImmutableList items, LambdaExpression field, SqlAlias alias) 21 | { 22 | return CreateField(items, BuildSqlField(field, alias)); 23 | } 24 | 25 | private static SqlFilterField> CreateField( 26 | ImmutableList items, ITypedSqlField field) 27 | { 28 | return new SqlFilterField>(items, field, i => new SqlFilter(i)); 29 | } 30 | 31 | //----------------------------------------------------------------------------------------------------- 32 | 33 | public static SqlFilterField> From( 34 | Expression> field, SqlAlias alias = null) 35 | { 36 | return CreateField(ImmutableList.Empty, BuildSqlField(field, alias)); 37 | } 38 | 39 | public static SqlFilterField> From(SqlField field) 40 | { 41 | return CreateField(ImmutableList.Empty, field); 42 | } 43 | 44 | public static SqlFilterField> From(ITypedSqlField field) 45 | { 46 | CheckField(field); 47 | return CreateField(ImmutableList.Empty, field); 48 | } 49 | 50 | public static SqlFilterField> From(ITypedSqlField field) 51 | { 52 | return From(field); 53 | } 54 | 55 | //----------------------------------------------------------------------------------------------------- 56 | 57 | public SqlFilterField> And(Expression> field, SqlAlias alias = null) 58 | { 59 | return CreateField(AddItem(SqlFilterItems.And), field, alias); 60 | } 61 | 62 | public SqlFilterField> And(SqlField field) 63 | { 64 | return CreateField(AddItem(SqlFilterItems.And), field); 65 | } 66 | 67 | public SqlFilterField> And(ITypedSqlField field) 68 | { 69 | CheckField(field); 70 | return CreateField(AddItem(SqlFilterItems.And), field); 71 | } 72 | 73 | public SqlFilterField> And(ITypedSqlField field) 74 | { 75 | return And(field); 76 | } 77 | 78 | public SqlFilter And(SqlFilter filter) 79 | { 80 | return new SqlFilter(AddItem(SqlFilterItems.And).AddRange(filter.FilterItems)); 81 | } 82 | 83 | public MultitableSqlFilter And(SqlFilterBase filter) 84 | { 85 | return new MultitableSqlFilter(AddItem(SqlFilterItems.And).AddRange(filter.FilterItems)); 86 | } 87 | 88 | //----------------------------------------------------------------------------------------------------- 89 | 90 | public SqlFilterField> Or(Expression> field, SqlAlias alias = null) 91 | { 92 | return CreateField(AddItem(SqlFilterItems.Or), field, alias); 93 | } 94 | 95 | public SqlFilterField> Or(SqlField field) 96 | { 97 | return CreateField(AddItem(SqlFilterItems.Or), field); 98 | } 99 | 100 | public SqlFilterField> Or(ITypedSqlField field) 101 | { 102 | CheckField(field); 103 | return CreateField(AddItem(SqlFilterItems.Or), field); 104 | } 105 | 106 | public SqlFilterField> Or(ITypedSqlField field) 107 | { 108 | return Or(field); 109 | } 110 | 111 | public SqlFilter Or(SqlFilter filter) 112 | { 113 | return new SqlFilter(AddItem(SqlFilterItems.Or).AddRange(filter.FilterItems)); 114 | } 115 | 116 | public MultitableSqlFilter Or(SqlFilterBase filter) 117 | { 118 | return new MultitableSqlFilter(AddItem(SqlFilterItems.Or).AddRange(filter.FilterItems)); 119 | } 120 | 121 | //----------------------------------------------------------------------------------------------------- 122 | 123 | public SqlFilter AndGroup(SqlFilter filter) 124 | { 125 | var items = AddItem(SqlFilterItems.And) 126 | .Add(SqlFilterItems.Build("(")) 127 | .AddRange(filter.FilterItems) 128 | .Add(SqlFilterItems.Build(")")); 129 | return new SqlFilter(items); 130 | } 131 | 132 | public MultitableSqlFilter AndGroup(SqlFilterBase filter) 133 | { 134 | var items = AddItem(SqlFilterItems.And) 135 | .Add(SqlFilterItems.Build("(")) 136 | .AddRange(filter.FilterItems) 137 | .Add(SqlFilterItems.Build(")")); 138 | return new MultitableSqlFilter(items); 139 | } 140 | 141 | //----------------------------------------------------------------------------------------------------- 142 | 143 | public SqlFilter OrGroup(SqlFilter filter) 144 | { 145 | var items = AddItem(SqlFilterItems.Or) 146 | .Add(SqlFilterItems.Build("(")) 147 | .AddRange(filter.FilterItems) 148 | .Add(SqlFilterItems.Build(")")); 149 | return new SqlFilter(items); 150 | } 151 | 152 | public MultitableSqlFilter OrGroup(SqlFilterBase filter) 153 | { 154 | var items = AddItem(SqlFilterItems.Or) 155 | .Add(SqlFilterItems.Build("(")) 156 | .AddRange(filter.FilterItems) 157 | .Add(SqlFilterItems.Build(")")); 158 | return new MultitableSqlFilter(items); 159 | } 160 | 161 | //----------------------------------------------------------------------------------------------------- 162 | 163 | public SqlFilter WithoutAliases() 164 | { 165 | var filter = new SqlFilter(FilterItems); 166 | filter.MustBeWithoutAliases = true; 167 | return filter; 168 | } 169 | 170 | public SqlFilter WithAliases() 171 | { 172 | var filter = new SqlFilter(FilterItems); 173 | filter.MustBeWithoutAliases = false; 174 | return filter; 175 | } 176 | 177 | //----------------------------------------------------------------------------------------------------- 178 | 179 | public SqlFilter WithParameterPrefix(string prefix) 180 | { 181 | var filter = new SqlFilter(FilterItems); 182 | filter.ParamPrefix = prefix; 183 | return filter; 184 | } 185 | 186 | public MultitableSqlFilter AsMultitable() => new MultitableSqlFilter(FilterItems); 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /LambdaSql/Filter/SqlFilterBase.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Collections.Immutable; 3 | using System.Data.Common; 4 | using System.Diagnostics; 5 | using System.Linq; 6 | using System.Linq.Expressions; 7 | using System.Text; 8 | using LambdaSql.Field; 9 | using LambdaSql.Filter.SqlFilterItem; 10 | 11 | namespace LambdaSql.Filter 12 | { 13 | internal delegate ISqlFilterItem SqlFilterItemCallback(SqlFilterConfiguration configuration); 14 | 15 | public class SqlFilterBase : ISqlFilter 16 | { 17 | protected bool MustBeWithoutAliases = false; 18 | protected string ParamPrefix = "p"; 19 | 20 | internal SqlFilterBase(ImmutableList sqlFilterItems) 21 | { 22 | FilterItems = sqlFilterItems; 23 | } 24 | 25 | 26 | internal ImmutableList FilterItems { get; } 27 | 28 | private string _rawSql; 29 | public string RawSql 30 | { 31 | get 32 | { 33 | if (_rawSql == null) 34 | { 35 | var configuration = new SqlFilterConfiguration 36 | { 37 | WithoutAliases = MustBeWithoutAliases, 38 | WithoutParameters = true 39 | }; 40 | 41 | _rawSql = FilterItems.Aggregate(new StringBuilder(), 42 | (sb, item) => sb.Append(item(configuration).Expression)).ToString(); 43 | } 44 | return _rawSql; 45 | } 46 | } 47 | 48 | private DbParameter[] _parameters; 49 | public DbParameter[] Parameters 50 | { 51 | get 52 | { 53 | if (_parameters == null) 54 | FillParametricFilter(); 55 | return _parameters; 56 | } 57 | } 58 | 59 | private string _parametricSql; 60 | public string ParametricSql 61 | { 62 | get 63 | { 64 | if (_parametricSql == null) 65 | FillParametricFilter(); 66 | return _parametricSql; 67 | } 68 | } 69 | 70 | private void FillParametricFilter() 71 | { 72 | var configuration = new SqlFilterConfiguration 73 | { 74 | WithoutAliases = MustBeWithoutAliases, 75 | WithoutParameters = false 76 | }; 77 | 78 | var filterSb = new StringBuilder(); 79 | var parameters = new List(); 80 | var counter = 0; 81 | foreach (var itemFunc in FilterItems) 82 | { 83 | var item = itemFunc(configuration); 84 | parameters.AddRange(item.Parameters.Select(p => 85 | { 86 | p.ParameterName = $"@{ParamPrefix}{counter}"; 87 | counter++; 88 | return p; 89 | })); 90 | filterSb.Append(item.Expression); 91 | } 92 | 93 | _parametricSql = filterSb.ToString(); 94 | _parameters = parameters.ToArray(); 95 | } 96 | 97 | public override string ToString() => RawSql; 98 | 99 | protected static SqlAlias CheckAlias(SqlAlias alias) 100 | => alias ?? MetadataProvider.Instance.AliasFor(); 101 | 102 | protected static string GetFieldName(LambdaExpression field, ISqlAlias alias) 103 | => alias.Value + "." + MetadataProvider.Instance.GetPropertyName(field); 104 | 105 | internal static ITypedSqlField BuildSqlField(LambdaExpression field, SqlAlias alias) 106 | { 107 | alias = CheckAlias(alias); 108 | var sqlField = new SqlField { Alias = alias, Name = MetadataProvider.Instance.GetPropertyName(field)}; 109 | return sqlField; 110 | } 111 | 112 | protected static void CheckField(ITypedSqlField field) 113 | { 114 | Debug.Assert(field != null, "SqlField is null."); 115 | Debug.Assert(typeof(TEntity) == field.EntityType, 116 | $"Incorrect SqlField, entity type does not match. Expected: {typeof(TEntity)}; Actual: {field.EntityType}."); 117 | Debug.Assert(typeof(TFieldType).IsAssignableFrom(field.FieldType), 118 | $"Incorrect SqlField, field type does not match. Expected: {typeof(TFieldType)}; Actual: {field.FieldType}."); 119 | } 120 | 121 | internal ImmutableList AddItem(SqlFilterItemCallback item) 122 | => FilterItems.Count == 0 ? FilterItems : FilterItems.Add(item); 123 | 124 | ISqlFilter ISqlFilter.WithParameterPrefix(string prefix) 125 | { 126 | var filter = (SqlFilterBase)MemberwiseClone(); 127 | filter.ParamPrefix = prefix; 128 | return filter; 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /LambdaSql/Filter/SqlFilterBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.Immutable; 4 | using System.Linq; 5 | using System.Linq.Expressions; 6 | using LambdaSql.Field; 7 | using LambdaSql.Filter.SqlFilterItem; 8 | 9 | namespace LambdaSql.Filter 10 | { 11 | internal class SqlFilterBuilder 12 | { 13 | private readonly ImmutableList _sqlFilterItems; 14 | private readonly ISqlField _sqlField; 15 | private readonly Func, SqlFilterBase> _filterCreatorFunc; 16 | 17 | public SqlFilterBuilder(ImmutableList sqlFilterItems, ISqlField sqlField, Func, SqlFilterBase> filterCreatorFunc) 18 | { 19 | _sqlFilterItems = sqlFilterItems; 20 | _sqlField = sqlField; 21 | _filterCreatorFunc = filterCreatorFunc; 22 | } 23 | 24 | private ISqlAlias CheckAlias(ISqlAlias alias) 25 | => alias ?? MetadataProvider.Instance.AliasFor(); 26 | 27 | private T BuildFilter(ImmutableList sqlFilterItems) 28 | => (dynamic)_filterCreatorFunc(sqlFilterItems); 29 | 30 | public T BuildFilter(string expression, ISqlField sqlField) 31 | => BuildFilter(_sqlFilterItems.Add(config 32 | => new SqlFilterItem.SqlFilterItem(expression, SqlFilterParameter.Create(config, sqlField)))); 33 | 34 | public T BuildFilter(string expression, params SqlFilterParameter[] args) 35 | => BuildFilter(_sqlFilterItems.Add(config => new SqlFilterItem.SqlFilterItem(expression, args))); 36 | 37 | public T BuildFilter(string expression, Func args) 38 | => BuildFilter(_sqlFilterItems.Add(config => new SqlFilterItem.SqlFilterItem(expression, args.Invoke(config)))); 39 | 40 | public T ComparisonFilter(string @operator, object value) 41 | { 42 | Func args = config => new[] 43 | { 44 | SqlFilterParameter.Create(config, _sqlField), 45 | SqlFilterParameter.Create(config, value) 46 | }; 47 | return BuildFilter("{0} " + @operator + " {1}", args); 48 | } 49 | 50 | public T ComparisonFilter(string @operator, ISqlField sqlField) 51 | { 52 | if (sqlField == null) throw new ArgumentNullException(nameof(sqlField)); 53 | Func args = config => new[] 54 | { 55 | SqlFilterParameter.Create(config, _sqlField), 56 | SqlFilterParameter.Create(config, sqlField) 57 | }; 58 | return BuildFilter("{0} " + @operator + " {1}", args); 59 | } 60 | 61 | public T ComparisonFilter(string @operator, LambdaExpression field, SqlAlias alias) 62 | => ComparisonFilter(@operator, field, alias); 63 | 64 | public T ComparisonFilter(string logicOperator, LambdaExpression field, ISqlAlias alias) 65 | { 66 | if (field == null) throw new ArgumentNullException(nameof(field)); 67 | alias = CheckAlias(alias); 68 | var sqlField = new SqlField() { Alias = alias, Name = MetadataProvider.Instance.GetPropertyName(field) }; 69 | return ComparisonFilter(logicOperator, sqlField); 70 | } 71 | 72 | public T ContainsFilter(string @operator, IEnumerable values) 73 | { 74 | Func args = config => 75 | { 76 | var list = new List(); 77 | list.Add(SqlFilterParameter.Create(config, _sqlField)); 78 | list.AddRange(values.Select(val => SqlFilterParameter.Create(config, val))); 79 | return list.ToArray(); 80 | }; 81 | 82 | var parameters = string.Join(",", values.Select((val, i) => $"{{{i + 1}}}")); 83 | return BuildFilter("{0} " + @operator + " (" + parameters + ")", args); 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /LambdaSql/Filter/SqlFilterConfiguration.cs: -------------------------------------------------------------------------------- 1 | namespace LambdaSql.Filter 2 | { 3 | internal class SqlFilterConfiguration 4 | { 5 | public bool WithoutAliases { get; set; } 6 | public bool WithoutParameters { get; set; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /LambdaSql/Filter/SqlFilterField.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.Immutable; 4 | using System.Linq; 5 | using System.Linq.Expressions; 6 | using LambdaSql.Field; 7 | using LambdaSql.Filter.SqlFilterItem; 8 | 9 | namespace LambdaSql.Filter 10 | { 11 | public class SqlFilterField 12 | where TResult : SqlFilterBase 13 | { 14 | private readonly ISqlField _sqlField; 15 | private readonly SqlFilterBuilder _sqlFilterBuilder; 16 | 17 | internal SqlFilterField(ImmutableList sqlFilterItems, ISqlField sqlField, 18 | Func, TResult> filterCreatorFunc) 19 | { 20 | _sqlField = sqlField; 21 | _sqlFilterBuilder = new SqlFilterBuilder(sqlFilterItems, sqlField, filterCreatorFunc); 22 | } 23 | 24 | //---------------------------------------------------------------------------- 25 | 26 | public TResult SatisfyLambda(Func filter) 27 | { 28 | if (filter == null) throw new ArgumentNullException(nameof(filter)); 29 | return _sqlFilterBuilder.BuildFilter(filter(_sqlField)); 30 | } 31 | 32 | //---------------------------------------------------------------------------- 33 | 34 | public TResult IsNull() => _sqlFilterBuilder.BuildFilter("{0} IS NULL", _sqlField); 35 | 36 | public TResult IsNotNull() => _sqlFilterBuilder.BuildFilter("{0} IS NOT NULL", _sqlField); 37 | 38 | //---------------------------------------------------------------------------- 39 | 40 | public TResult Like(string value) 41 | { 42 | if (string.IsNullOrWhiteSpace(value)) throw new ArgumentException(nameof(value)); 43 | SqlFilterParameter[] GetParameters(SqlFilterConfiguration config) => new[] 44 | { 45 | SqlFilterParameter.Create(config, _sqlField), SqlFilterParameter.Create(config, value) 46 | }; 47 | 48 | return _sqlFilterBuilder.BuildFilter("{0} LIKE {1}", GetParameters); 49 | } 50 | 51 | //---------------------------------------------------------------------------- 52 | 53 | private const string EQUAL_OPERATOR = "="; 54 | 55 | public TResult EqualTo(TFieldType value) 56 | => _sqlFilterBuilder.ComparisonFilter(EQUAL_OPERATOR, value); 57 | 58 | public TResult EqualTo(Expression> field, SqlAlias alias = null) 59 | => _sqlFilterBuilder.ComparisonFilter(EQUAL_OPERATOR, field, alias); 60 | 61 | public TResult EqualTo(SqlField field) 62 | => _sqlFilterBuilder.ComparisonFilter(EQUAL_OPERATOR, field); 63 | 64 | public TResult EqualTo(Expression> field, SqlAlias alias = null) 65 | => _sqlFilterBuilder.ComparisonFilter(EQUAL_OPERATOR, field, alias); 66 | 67 | public TResult EqualTo(SqlField field) 68 | => _sqlFilterBuilder.ComparisonFilter(EQUAL_OPERATOR, field); 69 | 70 | public TResult EqualTo(ISqlField field) 71 | => _sqlFilterBuilder.ComparisonFilter(EQUAL_OPERATOR, field); 72 | 73 | 74 | //---------------------------------------------------------------------------- 75 | 76 | private const string NOT_EQUAL_OPERATOR = "<>"; 77 | 78 | public TResult NotEqualTo(TFieldType value) 79 | => _sqlFilterBuilder.ComparisonFilter(NOT_EQUAL_OPERATOR, value); 80 | 81 | public TResult NotEqualTo(Expression> field, SqlAlias alias = null) 82 | => _sqlFilterBuilder.ComparisonFilter(NOT_EQUAL_OPERATOR, field, alias); 83 | 84 | public TResult NotEqualTo(SqlField field) 85 | => _sqlFilterBuilder.ComparisonFilter(NOT_EQUAL_OPERATOR, field); 86 | 87 | public TResult NotEqualTo(Expression> field, SqlAlias alias = null) 88 | => _sqlFilterBuilder.ComparisonFilter(NOT_EQUAL_OPERATOR, field, alias); 89 | 90 | public TResult NotEqualTo(SqlField field) 91 | => _sqlFilterBuilder.ComparisonFilter(NOT_EQUAL_OPERATOR, field); 92 | 93 | public TResult NotEqualTo(ISqlField field) 94 | => _sqlFilterBuilder.ComparisonFilter(NOT_EQUAL_OPERATOR, field); 95 | 96 | //---------------------------------------------------------------------------- 97 | 98 | private const string GREATER_THAN_OPERATOR = ">"; 99 | 100 | public TResult GreaterThan(TFieldType value) 101 | => _sqlFilterBuilder.ComparisonFilter(GREATER_THAN_OPERATOR, value); 102 | 103 | public TResult GreaterThan(Expression> field, SqlAlias alias = null) 104 | => _sqlFilterBuilder.ComparisonFilter(GREATER_THAN_OPERATOR, field, alias); 105 | 106 | public TResult GreaterThan(SqlField field) 107 | => _sqlFilterBuilder.ComparisonFilter(GREATER_THAN_OPERATOR, field); 108 | 109 | public TResult GreaterThan(Expression> field, SqlAlias alias = null) 110 | => _sqlFilterBuilder.ComparisonFilter(GREATER_THAN_OPERATOR, field, alias); 111 | 112 | public TResult GreaterThan(SqlField field) 113 | => _sqlFilterBuilder.ComparisonFilter(GREATER_THAN_OPERATOR, field); 114 | 115 | public TResult GreaterThan(ISqlField field) 116 | => _sqlFilterBuilder.ComparisonFilter(GREATER_THAN_OPERATOR, field); 117 | 118 | //---------------------------------------------------------------------------- 119 | 120 | private const string GREATER_THAN_OR_EQUAL_OPERATOR = ">="; 121 | 122 | public TResult GreaterThanOrEqual(TFieldType value) 123 | => _sqlFilterBuilder.ComparisonFilter(GREATER_THAN_OR_EQUAL_OPERATOR, value); 124 | 125 | public TResult GreaterThanOrEqual(Expression> field, SqlAlias alias = null) 126 | => _sqlFilterBuilder.ComparisonFilter(GREATER_THAN_OR_EQUAL_OPERATOR, field, alias); 127 | 128 | public TResult GreaterThanOrEqual(SqlField field) 129 | => _sqlFilterBuilder.ComparisonFilter(GREATER_THAN_OR_EQUAL_OPERATOR, field); 130 | 131 | public TResult GreaterThanOrEqual(Expression> field, SqlAlias alias = null) 132 | => _sqlFilterBuilder.ComparisonFilter(GREATER_THAN_OR_EQUAL_OPERATOR, field, alias); 133 | 134 | public TResult GreaterThanOrEqual(SqlField field) 135 | => _sqlFilterBuilder.ComparisonFilter(GREATER_THAN_OR_EQUAL_OPERATOR, field); 136 | 137 | public TResult GreaterThanOrEqual(ISqlField field) 138 | => _sqlFilterBuilder.ComparisonFilter(GREATER_THAN_OR_EQUAL_OPERATOR, field); 139 | 140 | //---------------------------------------------------------------------------- 141 | 142 | private const string LESS_THAN_OPERATOR = "<"; 143 | 144 | public TResult LessThan(TFieldType value) 145 | => _sqlFilterBuilder.ComparisonFilter(LESS_THAN_OPERATOR, value); 146 | 147 | public TResult LessThan(Expression> field, SqlAlias alias = null) 148 | => _sqlFilterBuilder.ComparisonFilter(LESS_THAN_OPERATOR, field, alias); 149 | 150 | public TResult LessThan(SqlField field) 151 | => _sqlFilterBuilder.ComparisonFilter(LESS_THAN_OPERATOR, field); 152 | 153 | public TResult LessThan(Expression> field, SqlAlias alias = null) 154 | => _sqlFilterBuilder.ComparisonFilter(LESS_THAN_OPERATOR, field, alias); 155 | 156 | public TResult LessThan(SqlField field) 157 | => _sqlFilterBuilder.ComparisonFilter(LESS_THAN_OPERATOR, field); 158 | 159 | public TResult LessThan(ISqlField field) 160 | => _sqlFilterBuilder.ComparisonFilter(LESS_THAN_OPERATOR, field); 161 | 162 | //---------------------------------------------------------------------------- 163 | 164 | private const string LESS_THAN_OR_EQUAL_OPERATOR = "<="; 165 | 166 | public TResult LessThanOrEqual(TFieldType value) 167 | => _sqlFilterBuilder.ComparisonFilter(LESS_THAN_OR_EQUAL_OPERATOR, value); 168 | 169 | public TResult LessThanOrEqual(Expression> field, SqlAlias alias = null) 170 | => _sqlFilterBuilder.ComparisonFilter(LESS_THAN_OR_EQUAL_OPERATOR, field, alias); 171 | 172 | public TResult LessThanOrEqual(SqlField field) 173 | => _sqlFilterBuilder.ComparisonFilter(LESS_THAN_OR_EQUAL_OPERATOR, field); 174 | 175 | public TResult LessThanOrEqual(Expression> field, SqlAlias alias = null) 176 | => _sqlFilterBuilder.ComparisonFilter(LESS_THAN_OR_EQUAL_OPERATOR, field, alias); 177 | 178 | public TResult LessThanOrEqual(SqlField field) 179 | => _sqlFilterBuilder.ComparisonFilter(LESS_THAN_OR_EQUAL_OPERATOR, field); 180 | 181 | public TResult LessThanOrEqual(ISqlField field) 182 | => _sqlFilterBuilder.ComparisonFilter(LESS_THAN_OR_EQUAL_OPERATOR, field); 183 | 184 | //---------------------------------------------------------------------------- 185 | 186 | public TResult In(params TFieldType[] values) 187 | { 188 | if (values?.Any() != true) throw new ArgumentException("Sequence contains no elements"); 189 | return _sqlFilterBuilder.ContainsFilter("IN", values); 190 | } 191 | 192 | public TResult In(IEnumerable values) 193 | { 194 | if (values?.Any() != true) throw new ArgumentException("Sequence contains no elements"); 195 | return _sqlFilterBuilder.ContainsFilter("IN", values); 196 | } 197 | 198 | //---------------------------------------------------------------------------- 199 | 200 | public TResult NotIn(params TFieldType[] values) 201 | { 202 | if (values?.Any() != true) throw new ArgumentException("Sequence contains no elements"); 203 | return _sqlFilterBuilder.ContainsFilter("NOT IN", values); 204 | } 205 | 206 | public TResult NotIn(IEnumerable values) 207 | { 208 | if (values?.Any() != true) throw new ArgumentException("Sequence contains no elements"); 209 | return _sqlFilterBuilder.ContainsFilter("NOT IN", values); 210 | } 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /LambdaSql/Filter/SqlFilterItem/ConstSqlFilterItem.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Data.Common; 3 | using System.Linq; 4 | 5 | namespace LambdaSql.Filter.SqlFilterItem 6 | { 7 | internal class ConstSqlFilterItem : ISqlFilterItem 8 | { 9 | public ConstSqlFilterItem(string value) 10 | { 11 | Expression = value; 12 | } 13 | 14 | public string Expression { get; } 15 | 16 | public IEnumerable Parameters { get; } = Enumerable.Empty(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /LambdaSql/Filter/SqlFilterItem/ISqlFilterItem.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Data.Common; 3 | 4 | namespace LambdaSql.Filter.SqlFilterItem 5 | { 6 | internal interface ISqlFilterItem 7 | { 8 | string Expression { get; } 9 | IEnumerable Parameters { get; } 10 | } 11 | } -------------------------------------------------------------------------------- /LambdaSql/Filter/SqlFilterItem/SqlFilterItem.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Data.Common; 3 | using System.Linq; 4 | 5 | namespace LambdaSql.Filter.SqlFilterItem 6 | { 7 | internal class SqlFilterItem : ISqlFilterItem 8 | { 9 | private readonly string _expression; 10 | private readonly SqlFilterParameter[] _sqlFilterParameters; 11 | 12 | public SqlFilterItem(string expression, params SqlFilterParameter[] args) 13 | { 14 | _expression = expression; 15 | _sqlFilterParameters = args; 16 | Parameters = args.Select(p => p.Parameter).Where(p => p != null); 17 | } 18 | 19 | public string Expression => string.Format(_expression, _sqlFilterParameters); 20 | 21 | public IEnumerable Parameters { get; } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /LambdaSql/Filter/SqlFilterItem/SqlFilterItems.cs: -------------------------------------------------------------------------------- 1 | namespace LambdaSql.Filter.SqlFilterItem 2 | { 3 | internal static class SqlFilterItems 4 | { 5 | public static SqlFilterItemCallback And = config => new ConstSqlFilterItem(" AND "); 6 | public static SqlFilterItemCallback Or = config => new ConstSqlFilterItem(" OR "); 7 | public static SqlFilterItemCallback Build(string value) => config => new ConstSqlFilterItem(value); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /LambdaSql/Filter/SqlFilterItem/SqlFilterParameter.cs: -------------------------------------------------------------------------------- 1 | using System.Data.Common; 2 | using LambdaSql.Field; 3 | 4 | namespace LambdaSql.Filter.SqlFilterItem 5 | { 6 | internal abstract class SqlFilterParameter 7 | { 8 | private readonly SqlFilterConfiguration _configuration; 9 | 10 | private SqlFilterParameter(SqlFilterConfiguration configuration) 11 | { 12 | _configuration = configuration; 13 | } 14 | 15 | public abstract string Value { get; } 16 | public abstract DbParameter Parameter { get; } 17 | 18 | public static SqlFilterParameter Create(SqlFilterConfiguration configuration, ISqlField sqlField) 19 | => new SqlFieldParameter(configuration, sqlField); 20 | 21 | public static SqlFilterParameter Create(SqlFilterConfiguration configuration, object paramValue) 22 | { 23 | var dbParameter = MetadataProvider.Instance.CreateDbParameter(); 24 | dbParameter.Value = paramValue; 25 | return new DbParamParameter(configuration, dbParameter); 26 | } 27 | 28 | public override string ToString() => Value; 29 | 30 | //----------------------------------------------------------------------------------- 31 | 32 | private class SqlFieldParameter : SqlFilterParameter 33 | { 34 | private readonly ISqlField _sqlField; 35 | 36 | public SqlFieldParameter(SqlFilterConfiguration configuration, ISqlField sqlField) : base(configuration) 37 | { 38 | _sqlField = sqlField; 39 | } 40 | 41 | public override DbParameter Parameter => null; 42 | 43 | public override string Value 44 | { 45 | get 46 | { 47 | var field = _sqlField.Clone(); 48 | if (_configuration.WithoutAliases) 49 | field.Alias = null; 50 | return field.ShortString; 51 | } 52 | } 53 | } 54 | 55 | private class DbParamParameter : SqlFilterParameter 56 | { 57 | private readonly DbParameter _dbParameter; 58 | 59 | public DbParamParameter(SqlFilterConfiguration configuration, DbParameter dbParameter) 60 | : base(configuration) 61 | { 62 | _dbParameter = dbParameter; 63 | } 64 | 65 | public override DbParameter Parameter 66 | => _configuration.WithoutParameters ? null : _dbParameter; 67 | 68 | public override string Value => _configuration.WithoutParameters 69 | ? MetadataProvider.Instance.ParameterToString(_dbParameter.Value) 70 | : _dbParameter.ParameterName; 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /LambdaSql/IMetadataProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Data; 3 | using System.Data.Common; 4 | using System.Linq.Expressions; 5 | 6 | namespace LambdaSql 7 | { 8 | public interface IMetadataProvider 9 | { 10 | string GetTableName(); 11 | string GetTableName(Type entityType); 12 | string GetPropertyName(LambdaExpression propertyExpression); 13 | string GetPropertyName(MemberExpression propertyExpression); 14 | DbParameter CreateDbParameter(); 15 | string ParameterToString(object value, DbType? dbType = null); 16 | SqlAlias AliasFor(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /LambdaSql/ISqlSelect.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Data.Common; 3 | using System.Linq.Expressions; 4 | using LambdaSql.Field; 5 | using LambdaSql.Filter; 6 | using LambdaSql.QueryBuilder; 7 | 8 | namespace LambdaSql 9 | { 10 | public interface ISqlSelect 11 | { 12 | string RawSql { get; } 13 | string ParametricSql { get; } 14 | SqlSelectInfo Info { get; } 15 | Type EntityType { get; } 16 | DbParameter[] Parameters { get; } 17 | 18 | 19 | ISqlSelect Extend(Func decorationCallback); 20 | ISqlSelect Distinct(bool isDistinct); 21 | 22 | ISqlSelect AddFields(params ISqlField[] fields); 23 | ISqlSelect AddFields(SqlAlias alias = null, 24 | params Expression>[] fields); 25 | 26 | ISqlSelect GroupBy(params ISqlField[] fields); 27 | ISqlSelect GroupBy(SqlAlias alias = null, 28 | params Expression>[] fields); 29 | 30 | ISqlSelect OrderBy(params ISqlField[] fields); 31 | ISqlSelect OrderBy(SqlAlias alias = null, 32 | params Expression>[] fields); 33 | 34 | ISqlSelect Join(JoinType joinType, ISqlFilter condition, SqlAlias joinAlias = null); 35 | 36 | ISqlSelect Where(ISqlFilter filter); 37 | ISqlSelect Having(ISqlFilter filter); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /LambdaSql/LambdaSql.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net40 5 | LambdaSql 6 | LambdaSql 7 | 0.3.1.0 8 | Sergey Aseev (Serg046) 9 | https://github.com/Serg046/LambdaSql 10 | https://github.com/Serg046/LambdaSql 11 | git 12 | LambdaSql SQL ORM CRUD QueryObject Query Dapper SqlFilter SQLGeneration 13 | true 14 | snupkg 15 | true 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /LambdaSql/LibHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq.Expressions; 3 | 4 | namespace LambdaSql 5 | { 6 | internal static class LibHelper 7 | { 8 | public static MemberExpression GetMemberExpression(LambdaExpression expression) 9 | => GetMemberExpression(expression.Body); 10 | 11 | public static MemberExpression GetMemberExpression(Expression expression) 12 | { 13 | MemberExpression result; 14 | 15 | var memberExpression = expression as MemberExpression; 16 | if (memberExpression != null) 17 | result = memberExpression; 18 | else 19 | { 20 | var unary = expression as UnaryExpression; 21 | if (unary != null && unary.NodeType == ExpressionType.Convert && unary.Operand is MemberExpression) 22 | { 23 | result = (MemberExpression)unary.Operand; 24 | } 25 | else 26 | { 27 | throw new NotSupportedException($"'{expression.GetType().FullName}' is not supported for member expression"); 28 | } 29 | } 30 | return result; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LambdaSql/MetadataProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Data; 3 | using System.Data.Common; 4 | using System.Data.SqlClient; 5 | using System.Linq; 6 | using System.Linq.Expressions; 7 | 8 | namespace LambdaSql 9 | { 10 | public class MetadataProvider : IMetadataProvider 11 | { 12 | private static readonly DbType[] _quotedParameterTypes = { 13 | DbType.AnsiString, DbType.Date, 14 | DbType.DateTime, DbType.Guid, DbType.String, 15 | DbType.AnsiStringFixedLength, DbType.StringFixedLength 16 | }; 17 | 18 | private SqlAliasContainer _aliasContainer; 19 | 20 | static MetadataProvider() 21 | { 22 | Instance = new MetadataProvider(); 23 | } 24 | 25 | protected MetadataProvider() 26 | { 27 | _aliasContainer = new SqlAliasContainer(); 28 | } 29 | 30 | protected MetadataProvider(SqlAliasContainerBuilder aliasContainerBuilder) 31 | { 32 | if (aliasContainerBuilder == null) throw new ArgumentNullException(nameof(aliasContainerBuilder)); 33 | if (aliasContainerBuilder.RegisteredAliases?.Any() != true) throw new ArgumentException("RegisteredAliases contains no elements"); 34 | _aliasContainer = new SqlAliasContainer(aliasContainerBuilder.RegisteredAliases); 35 | } 36 | 37 | public static IMetadataProvider Instance { get; private set; } 38 | 39 | public static void Initialize(SqlAliasContainerBuilder aliasContainerBuilder) 40 | { 41 | if (aliasContainerBuilder == null) throw new ArgumentNullException(nameof(aliasContainerBuilder)); 42 | if (aliasContainerBuilder.RegisteredAliases?.Any() != true) throw new ArgumentException("RegisteredAliases contains no elements"); 43 | var provider = Instance as MetadataProvider; 44 | if (provider == null) throw new InvalidOperationException("The method supports only the default metadata provider"); 45 | provider._aliasContainer = new SqlAliasContainer(aliasContainerBuilder.RegisteredAliases); 46 | } 47 | 48 | public static void Initialize(IMetadataProvider metadataProvider) 49 | { 50 | Instance = metadataProvider ?? throw new ArgumentNullException(nameof(metadataProvider)); 51 | } 52 | 53 | public virtual string GetTableName() 54 | { 55 | return GetTableName(typeof(TEntity)); 56 | } 57 | 58 | public virtual string GetTableName(Type entityType) 59 | { 60 | if (entityType == null) throw new ArgumentNullException(nameof(entityType)); 61 | return entityType.Name; 62 | } 63 | 64 | public virtual string GetPropertyName(LambdaExpression propertyExpression) 65 | { 66 | if (propertyExpression == null) throw new ArgumentNullException(nameof(propertyExpression)); 67 | return GetPropertyName(LibHelper.GetMemberExpression(propertyExpression)); 68 | } 69 | 70 | public virtual string GetPropertyName(MemberExpression memberExpression) 71 | { 72 | if (memberExpression == null) throw new ArgumentNullException(nameof(memberExpression)); 73 | return memberExpression.Member.Name; 74 | } 75 | 76 | public DbParameter CreateDbParameter() => new SqlParameter(); 77 | 78 | public virtual string ParameterToString(object value, DbType? dbType = null) 79 | { 80 | if (value == null) throw new ArgumentNullException(nameof(value)); 81 | 82 | string result; 83 | switch (value) 84 | { 85 | case bool flag: result = flag ? "1" : "0"; break; 86 | default: result = value.ToString(); break; 87 | } 88 | 89 | return (dbType.HasValue && _quotedParameterTypes.Contains(dbType.Value)) || 90 | (!dbType.HasValue && value is string) 91 | ? $"'{result}'" 92 | : result; 93 | } 94 | 95 | public virtual SqlAlias AliasFor() 96 | { 97 | return _aliasContainer.For(); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /LambdaSql/MsSqlServerExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace LambdaSql 2 | { 3 | public static class MsSqlServerExtensions 4 | { 5 | public static SqlSelect Top(this SqlSelect sqlSelect, int top) 6 | => sqlSelect.Extend(builder => builder.ModifySelectFields(fields => $"TOP {top} {fields}")); 7 | 8 | public static SqlSelect Top(this SqlSelect sqlSelect, int top) 9 | => sqlSelect.Extend(builder => builder.ModifySelectFields(fields => $"TOP {top} {fields}")); 10 | 11 | public static ISqlSelect Top(this ISqlSelect sqlSelect, int top) 12 | => sqlSelect.Extend(builder => builder.ModifySelectFields(fields => $"TOP {top} {fields}")); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /LambdaSql/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("LambdaSql.UnitTests")] 4 | -------------------------------------------------------------------------------- /LambdaSql/QueryBuilder/ISqlSelectQueryBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace LambdaSql.QueryBuilder 4 | { 5 | public interface ISqlSelectQueryBuilder : ICloneable 6 | { 7 | string Build(SqlSelectInfo info, bool parametric); 8 | 9 | ISqlSelectQueryBuilder ModifySelectFields(ModifyQueryPartCallback modificationCallback); 10 | ISqlSelectQueryBuilder ModifyJoins(ModifyQueryPartCallback modificationCallback); 11 | ISqlSelectQueryBuilder ModifyWhereFilters(ModifyQueryPartCallback modificationCallback); 12 | ISqlSelectQueryBuilder ModifyGroupByFields(ModifyQueryPartCallback modificationCallback); 13 | ISqlSelectQueryBuilder ModifyHavingFilters(ModifyQueryPartCallback modificationCallback); 14 | ISqlSelectQueryBuilder ModifyOrderByFields(ModifyQueryPartCallback modificationCallback); 15 | ISqlSelectQueryBuilder Modify(ModifyQueryPartCallback modificationCallback); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /LambdaSql/QueryBuilder/SqlSelectQueryBuilder.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace LambdaSql.QueryBuilder 4 | { 5 | internal class SqlSelectQueryBuilder : SqlSelectQueryBuilderBase 6 | { 7 | private readonly string _tableName; 8 | 9 | public SqlSelectQueryBuilder(string tableName) 10 | { 11 | _tableName = tableName; 12 | 13 | } 14 | 15 | public override string Build(SqlSelectInfo info, bool parametric) 16 | { 17 | CheckAsAliases(info); 18 | var sb = new StringBuilder("SELECT").Append(SEPARATOR_WITH_OFFSET) 19 | .Append(GetSelectedFields(info)).Append(SEPARATOR) 20 | .Append("FROM").Append(SEPARATOR_WITH_OFFSET) 21 | .Append(_tableName) 22 | .Append(" ").Append(info.Alias.Value); 23 | AppendJoins(sb, info); 24 | AppendWhere(sb, info, parametric); 25 | AppendGroupByFields(sb, info); 26 | AppendHaving(sb, info, parametric); 27 | AppendOrderByFields(sb, info); 28 | return GetQueryString(sb); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LambdaSql/QueryBuilder/SqlSelectQueryBuilderBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.Immutable; 4 | using System.Linq; 5 | using System.Text; 6 | using LambdaSql.Field; 7 | using LambdaSql.Filter; 8 | 9 | namespace LambdaSql.QueryBuilder 10 | { 11 | public delegate string ModifyQueryPartCallback(string query); 12 | 13 | internal abstract class SqlSelectQueryBuilderBase : ISqlSelectQueryBuilder 14 | { 15 | protected const string SEPARATOR = "\r\n"; 16 | protected const string SEPARATOR_WITH_OFFSET = "\r\n "; 17 | 18 | object ICloneable.Clone() 19 | { 20 | return MemberwiseClone(); 21 | } 22 | 23 | private SqlSelectQueryBuilderBase Clone() 24 | { 25 | return (SqlSelectQueryBuilderBase)MemberwiseClone(); 26 | } 27 | 28 | private ImmutableList _selectFieldsModificators = ImmutableList.Empty; 29 | public ISqlSelectQueryBuilder ModifySelectFields(ModifyQueryPartCallback modificationCallback) 30 | { 31 | var clone = Clone(); 32 | clone._selectFieldsModificators = clone._selectFieldsModificators.Add(modificationCallback); 33 | return clone; 34 | } 35 | 36 | private ImmutableList _groupByFieldsModificators = ImmutableList.Empty; 37 | public ISqlSelectQueryBuilder ModifyGroupByFields(ModifyQueryPartCallback modificationCallback) 38 | { 39 | var clone = Clone(); 40 | clone._groupByFieldsModificators = clone._groupByFieldsModificators.Add(modificationCallback); 41 | return clone; 42 | } 43 | 44 | private ImmutableList _orderByFieldsModificators = ImmutableList.Empty; 45 | public ISqlSelectQueryBuilder ModifyOrderByFields(ModifyQueryPartCallback modificationCallback) 46 | { 47 | var clone = Clone(); 48 | clone._orderByFieldsModificators = clone._orderByFieldsModificators.Add(modificationCallback); 49 | return clone; 50 | } 51 | 52 | private ImmutableList _joinModificators = ImmutableList.Empty; 53 | public ISqlSelectQueryBuilder ModifyJoins(ModifyQueryPartCallback modificationCallback) 54 | { 55 | var clone = Clone(); 56 | clone._joinModificators = clone._joinModificators.Add(modificationCallback); 57 | return clone; 58 | } 59 | 60 | private ImmutableList _whereModificators = ImmutableList.Empty; 61 | public ISqlSelectQueryBuilder ModifyWhereFilters(ModifyQueryPartCallback modificationCallback) 62 | { 63 | var clone = Clone(); 64 | clone._whereModificators = clone._whereModificators.Add(modificationCallback); 65 | return clone; 66 | } 67 | 68 | private ImmutableList _havingModificators = ImmutableList.Empty; 69 | public ISqlSelectQueryBuilder ModifyHavingFilters(ModifyQueryPartCallback modificationCallback) 70 | { 71 | var clone = Clone(); 72 | clone._havingModificators = clone._havingModificators.Add(modificationCallback); 73 | return clone; 74 | } 75 | 76 | private ImmutableList _wholeQueryModificators = ImmutableList.Empty; 77 | public ISqlSelectQueryBuilder Modify(ModifyQueryPartCallback modificationCallback) 78 | { 79 | var clone = Clone(); 80 | clone._wholeQueryModificators = clone._wholeQueryModificators.Add(modificationCallback); 81 | return clone; 82 | } 83 | 84 | public abstract string Build(SqlSelectInfo info, bool parametric); 85 | 86 | protected void CheckAsAliases(SqlSelectInfo info) 87 | { 88 | // ReSharper disable PossibleMultipleEnumeration 89 | var allFields = info.SelectFields().Union(info.GroupByFields()).Union(info.OrderByFields()); 90 | var intersect = info.AllAliases.Select(a => a.Value).Intersect(allFields.Select(a => a.AsAlias)); 91 | if (intersect.Any()) 92 | throw new IncorrectAliasException($"The following user aliases are incorrect: {string.Join(", ", intersect)}."); 93 | // ReSharper restore PossibleMultipleEnumeration 94 | } 95 | 96 | protected string GetSelectedFields(SqlSelectInfo info) 97 | { 98 | var selectFields = info.SelectFields(); 99 | var fields = selectFields.Count == 0 ? "*" : string.Join(", ", selectFields); 100 | fields = info.Distinct() ? $"DISTINCT {fields}" : fields; 101 | return _selectFieldsModificators.Aggregate(fields, (result, callback) => callback(result)); 102 | } 103 | 104 | protected void AppendJoins(StringBuilder querySb, SqlSelectInfo info) 105 | { 106 | if (info.Joins().Count > 0) 107 | { 108 | var joins = _joinModificators.Aggregate(string.Join(SEPARATOR, info.Joins()), 109 | (result, callback) => callback(result)); 110 | querySb.Append(SEPARATOR).Append(joins); 111 | } 112 | } 113 | 114 | private void AppendFilter(StringBuilder querySb, string clause, ISqlFilter filter, bool parametric, 115 | IReadOnlyList modificators) 116 | { 117 | if (filter != null) 118 | { 119 | var sql = parametric ? filter.ParametricSql : filter.RawSql; 120 | querySb.Append(SEPARATOR).Append(clause) 121 | .Append(SEPARATOR_WITH_OFFSET) 122 | .Append(modificators.Aggregate(sql, (result, callback) => callback(result))); 123 | } 124 | else if (modificators.Count > 0) 125 | { 126 | querySb.Append(modificators.Aggregate(string.Empty, (result, callback) => callback(result))); 127 | } 128 | } 129 | 130 | protected void AppendWhere(StringBuilder querySb, SqlSelectInfo info, bool parametric) 131 | { 132 | AppendFilter(querySb, "WHERE", info.Where(), parametric, _whereModificators); 133 | } 134 | 135 | protected void AppendHaving(StringBuilder querySb, SqlSelectInfo info, bool parametric) 136 | { 137 | AppendFilter(querySb, "HAVING", info.Having(), parametric, _havingModificators); 138 | } 139 | 140 | private void AppendFields(StringBuilder querySb, string clause, ICollection fields, 141 | IEnumerable modificators) 142 | { 143 | if (fields.Count > 0) 144 | { 145 | var joinedFields = string.Join(", ", fields.Select(f => f.ShortString)); 146 | querySb.Append(SEPARATOR) 147 | .Append(clause) 148 | .Append(SEPARATOR_WITH_OFFSET) 149 | .Append(modificators.Aggregate(joinedFields, (result, callback) => callback(result))); 150 | } 151 | } 152 | 153 | protected void AppendGroupByFields(StringBuilder querySb, SqlSelectInfo info) 154 | { 155 | AppendFields(querySb, "GROUP BY", info.GroupByFields(), _groupByFieldsModificators); 156 | } 157 | 158 | protected void AppendOrderByFields(StringBuilder querySb, SqlSelectInfo info) 159 | { 160 | AppendFields(querySb, "ORDER BY", info.OrderByFields(), _orderByFieldsModificators); 161 | } 162 | 163 | protected string GetQueryString(StringBuilder querySb) 164 | => _wholeQueryModificators.Aggregate(querySb.ToString(), (result, callback) => callback(result)); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /LambdaSql/QueryBuilder/SqlSelectWrapperQueryBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Text; 4 | 5 | namespace LambdaSql.QueryBuilder 6 | { 7 | internal class SqlSelectWrapperQueryBuilder : SqlSelectQueryBuilderBase 8 | { 9 | private readonly ISqlSelect _innerSqlSelect; 10 | 11 | public SqlSelectWrapperQueryBuilder(ISqlSelect innerSqlSelect) 12 | { 13 | _innerSqlSelect = innerSqlSelect; 14 | 15 | } 16 | 17 | public override string Build(SqlSelectInfo info, bool parametric) 18 | { 19 | CheckAsAliases(info); 20 | CheckSelectedFields(info); 21 | var innerSql = parametric ? _innerSqlSelect.ParametricSql : _innerSqlSelect.RawSql; 22 | var sb = new StringBuilder("SELECT").Append(SEPARATOR_WITH_OFFSET) 23 | .Append(GetSelectedFields(info)).Append(SEPARATOR) 24 | .Append("FROM") 25 | .Append(SEPARATOR).Append("(").Append(SEPARATOR_WITH_OFFSET) 26 | .Append(innerSql.Replace(SEPARATOR, SEPARATOR_WITH_OFFSET)) 27 | .Append(SEPARATOR).Append(") AS ").Append(info.Alias.Value); 28 | AppendJoins(sb, info); 29 | AppendWhere(sb, info, parametric); 30 | AppendGroupByFields(sb, info); 31 | AppendHaving(sb, info, parametric); 32 | AppendOrderByFields(sb, info); 33 | return GetQueryString(sb); 34 | } 35 | 36 | private void CheckSelectedFields(SqlSelectInfo info) 37 | { 38 | foreach (var selectField in info.SelectFields()) 39 | { 40 | if (!_innerSqlSelect.Info.SelectFields().Any(f => f.Name == selectField.Name 41 | && f.EntityType == selectField.EntityType)) 42 | { 43 | throw new InvalidOperationException( 44 | $"'{selectField}' is not set in the inner select query."); 45 | } 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /LambdaSql/SqlAlias.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace LambdaSql 4 | { 5 | public interface ISqlAlias 6 | { 7 | string Value { get; } 8 | Type EntityType { get; } 9 | } 10 | 11 | public class SqlAlias : ISqlAlias 12 | { 13 | public SqlAlias(string aliasName) 14 | { 15 | if (string.IsNullOrWhiteSpace(aliasName)) throw new ArgumentException(nameof(aliasName)); 16 | Value = aliasName; 17 | } 18 | 19 | public string Value { get; } 20 | 21 | public Type EntityType => typeof (T); 22 | 23 | public override string ToString() => Value; 24 | 25 | public override bool Equals(object obj) 26 | { 27 | var alias = obj as SqlAlias; 28 | return alias != null && Equals(alias); 29 | } 30 | 31 | public bool Equals(SqlAlias obj) 32 | { 33 | return obj != null && string.Equals(Value, obj.Value); 34 | } 35 | 36 | public override int GetHashCode() 37 | { 38 | return Value.GetHashCode(); 39 | } 40 | } 41 | 42 | public class SqlAlias : ISqlAlias 43 | { 44 | public SqlAlias(string aliasName) 45 | { 46 | if (string.IsNullOrWhiteSpace(aliasName)) throw new ArgumentException(nameof(aliasName)); 47 | Value = aliasName; 48 | } 49 | 50 | public Type EntityType => null; 51 | 52 | public string Value { get; } 53 | 54 | public override string ToString() => Value; 55 | 56 | public override bool Equals(object obj) 57 | { 58 | var alias = obj as SqlAlias; 59 | return alias != null && Equals(alias); 60 | } 61 | 62 | public bool Equals(SqlAlias obj) 63 | { 64 | return obj != null && string.Equals(Value, obj.Value); 65 | } 66 | 67 | public override int GetHashCode() 68 | { 69 | return Value.GetHashCode(); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /LambdaSql/SqlAliasContainer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace LambdaSql 6 | { 7 | internal class SqlAliasContainer 8 | { 9 | private readonly Dictionary _aliases; 10 | private readonly object _syncObject = new object(); 11 | 12 | public SqlAliasContainer() 13 | { 14 | _aliases = new Dictionary(); 15 | } 16 | 17 | public SqlAliasContainer(Dictionary aliases) 18 | { 19 | _aliases = aliases; 20 | } 21 | 22 | public SqlAlias For() 23 | { 24 | lock (_syncObject) 25 | { 26 | return TryGetAlias() ?? InitAlias(); 27 | } 28 | } 29 | 30 | private SqlAlias TryGetAlias() 31 | { 32 | var entityType = typeof(TEntity); 33 | return _aliases.ContainsKey(entityType) 34 | ? _aliases[entityType] as SqlAlias 35 | : null; 36 | } 37 | 38 | private SqlAlias InitAlias() 39 | { 40 | var entityType = typeof (TEntity); 41 | var alias = new SqlAlias(GenerateAliasName(entityType)); 42 | _aliases.Add(entityType, alias); 43 | return alias; 44 | } 45 | 46 | private string GenerateAliasName(Type entityType) 47 | { 48 | if (entityType == null) throw new ArgumentNullException(nameof(entityType)); 49 | 50 | var alias = MetadataProvider.Instance.GetTableName(entityType); 51 | if (alias.Length > 2) 52 | alias = alias.Substring(0, 2).ToLower(); 53 | var existingAlias = _aliases.SingleOrDefault(a => a.Value.Value == alias); 54 | if (existingAlias.Value != null) 55 | { 56 | throw new DuplicateAliasException( 57 | $"Please register aliases for the following types: {existingAlias.Key.FullName}, {entityType.FullName}. Use SqlAliasContainerBuilder."); 58 | } 59 | return alias; 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /LambdaSql/SqlAliasContainerBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace LambdaSql 5 | { 6 | public class SqlAliasContainerBuilder 7 | { 8 | private readonly Dictionary _aliases = new Dictionary(); 9 | 10 | public void Register(string alias) 11 | { 12 | var entityType = typeof(T); 13 | if (_aliases.ContainsKey(entityType)) 14 | throw new ArgumentException($"{entityType.FullName} is already registered"); 15 | _aliases.Add(entityType, new SqlAlias(alias)); 16 | } 17 | 18 | public Dictionary RegisteredAliases => _aliases; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /LambdaSql/SqlJoin.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using LambdaSql.Filter; 3 | 4 | namespace LambdaSql 5 | { 6 | public interface ISqlJoin 7 | { 8 | JoinType JoinType { get; } 9 | Type JoinEntityType { get; } 10 | ISqlAlias JoinAlias { get; } 11 | ISqlFilter JoinCondition { get; } 12 | } 13 | 14 | public enum JoinType 15 | { 16 | Inner, 17 | Left, 18 | Right, 19 | Full 20 | } 21 | 22 | public class SqlJoin : ISqlJoin 23 | { 24 | public SqlJoin(JoinType joinType, ISqlFilter joinCondition, SqlAlias joinAlias) 25 | { 26 | JoinType = joinType; 27 | JoinCondition = joinCondition ?? throw new ArgumentNullException(nameof(joinCondition)); 28 | JoinAlias = joinAlias ?? throw new ArgumentNullException(nameof(joinAlias)); 29 | } 30 | 31 | public JoinType JoinType { get; } 32 | public Type JoinEntityType => typeof (TJoin); 33 | public SqlAlias JoinAlias { get; } 34 | ISqlAlias ISqlJoin.JoinAlias => JoinAlias; 35 | public ISqlFilter JoinCondition { get; } 36 | 37 | public override string ToString() 38 | { 39 | var entity = MetadataProvider.Instance.GetTableName(JoinEntityType) + " " + JoinAlias.Value; 40 | return $"{JoinType.ToString().ToUpper()} JOIN\r\n {entity} ON {JoinCondition.RawSql }"; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /LambdaSql/SqlSelect.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Data.Common; 3 | using System.Linq.Expressions; 4 | using LambdaSql.Field; 5 | using LambdaSql.Filter; 6 | using LambdaSql.QueryBuilder; 7 | 8 | namespace LambdaSql 9 | { 10 | public class SqlSelect : SqlSelectBase, ISqlSelect 11 | { 12 | public SqlSelect() : this(new SqlSelectInfo(LambdaSql.MetadataProvider.Instance.AliasFor()), 13 | new SqlSelectQueryBuilder(LambdaSql.MetadataProvider.Instance.GetTableName())) 14 | { 15 | } 16 | 17 | private SqlSelect(SqlSelectInfo info, ISqlSelectQueryBuilder queryBuilder) : base(info, queryBuilder) 18 | { 19 | } 20 | 21 | private SqlSelect CreateSqlSelect(SqlSelectInfo info) => new SqlSelect(info, QueryBuilder); 22 | 23 | ISqlSelect ISqlSelect.Extend(Func decorationCallback) => Extend(decorationCallback); 24 | public SqlSelect Extend(Func decorationCallback) 25 | => new SqlSelect(Info, decorationCallback(QueryBuilder)); 26 | 27 | private DbParameter[] _parameters; 28 | public DbParameter[] Parameters => _parameters ?? (_parameters = GetFilterParameters(Info)); 29 | 30 | public override string ToString() => RawSql; 31 | 32 | public Type EntityType => typeof(T); 33 | 34 | public SqlSelect Distinct() 35 | { 36 | return Distinct(true); 37 | } 38 | 39 | ISqlSelect ISqlSelect.Distinct(bool isDistinct) => Distinct(isDistinct); 40 | public SqlSelect Distinct(bool isDistinct) 41 | { 42 | return CreateSqlSelect(Info.Distinct(isDistinct)); 43 | } 44 | 45 | //------------------------------------------------------------------------- 46 | 47 | ISqlSelect ISqlSelect.AddFields(ISqlField[] fields) => AddFields(fields); 48 | public SqlSelect AddFields(params ISqlField[] fields) 49 | { 50 | if (fields == null) throw new ArgumentNullException(nameof(fields)); 51 | return CreateSqlSelect(Info.SelectFields(fields)); 52 | } 53 | 54 | public SqlSelect AddFields(params Expression>[] fields) 55 | { 56 | if (fields == null) throw new ArgumentNullException(nameof(fields)); 57 | return AddFields(CreateSqlFields(MetadataProvider.AliasFor(), fields)); 58 | } 59 | 60 | public SqlSelect AddFields(params Expression>[] fields) 61 | { 62 | if (fields == null) throw new ArgumentNullException(nameof(fields)); 63 | return AddFields(CreateSqlFields(MetadataProvider.AliasFor(), fields)); 64 | } 65 | 66 | ISqlSelect ISqlSelect.AddFields(SqlAlias alias, 67 | params Expression>[] fields) 68 | { 69 | if (alias == null) 70 | MetadataProvider.AliasFor(); 71 | return AddFields(alias, fields); 72 | } 73 | 74 | public SqlSelect AddFields(SqlAlias alias, 75 | params Expression>[] fields) 76 | { 77 | if (alias == null) throw new ArgumentNullException(nameof(alias)); 78 | if (fields == null) throw new ArgumentNullException(nameof(fields)); 79 | return AddFields(CreateSqlFields(alias, fields)); 80 | } 81 | 82 | //------------------------------------------------------------------------- 83 | 84 | ISqlSelect ISqlSelect.GroupBy(ISqlField[] fields) => GroupBy(fields); 85 | public SqlSelect GroupBy(params ISqlField[] fields) 86 | { 87 | if (fields == null) throw new ArgumentNullException(nameof(fields)); 88 | return CreateSqlSelect(Info.GroupByFields(fields)); 89 | } 90 | 91 | public SqlSelect GroupBy(params Expression>[] fields) 92 | { 93 | if (fields == null) throw new ArgumentNullException(nameof(fields)); 94 | return GroupBy(CreateSqlFields(MetadataProvider.AliasFor(), fields)); 95 | } 96 | 97 | public SqlSelect GroupBy(params Expression>[] fields) 98 | { 99 | if (fields == null) throw new ArgumentNullException(nameof(fields)); 100 | return GroupBy(CreateSqlFields(MetadataProvider.AliasFor(), fields)); 101 | } 102 | 103 | ISqlSelect ISqlSelect.GroupBy(SqlAlias alias, 104 | params Expression>[] fields) 105 | { 106 | if (alias == null) 107 | MetadataProvider.AliasFor(); 108 | return GroupBy(alias, fields); 109 | } 110 | 111 | public SqlSelect GroupBy(SqlAlias alias, 112 | params Expression>[] fields) 113 | { 114 | if (alias == null) throw new ArgumentNullException(nameof(alias)); 115 | if (fields == null) throw new ArgumentNullException(nameof(fields)); 116 | return GroupBy(CreateSqlFields(alias, fields)); 117 | } 118 | 119 | //------------------------------------------------------------------------- 120 | 121 | ISqlSelect ISqlSelect.OrderBy(ISqlField[] fields) => OrderBy(fields); 122 | public SqlSelect OrderBy(params ISqlField[] fields) 123 | { 124 | if (fields == null) throw new ArgumentNullException(nameof(fields)); 125 | return CreateSqlSelect(Info.OrderByFields(fields)); 126 | } 127 | 128 | public SqlSelect OrderBy(params Expression>[] fields) 129 | { 130 | if (fields == null) throw new ArgumentNullException(nameof(fields)); 131 | return OrderBy(CreateSqlFields(MetadataProvider.AliasFor(), fields)); 132 | } 133 | 134 | public SqlSelect OrderBy(params Expression>[] fields) 135 | { 136 | if (fields == null) throw new ArgumentNullException(nameof(fields)); 137 | return OrderBy(CreateSqlFields(MetadataProvider.AliasFor(), fields)); 138 | } 139 | 140 | ISqlSelect ISqlSelect.OrderBy(SqlAlias alias, 141 | params Expression>[] fields) 142 | { 143 | if (alias == null) 144 | MetadataProvider.AliasFor(); 145 | return OrderBy(alias, fields); 146 | } 147 | 148 | public SqlSelect OrderBy(SqlAlias alias, 149 | params Expression>[] fields) 150 | { 151 | if (alias == null) throw new ArgumentNullException(nameof(alias)); 152 | if (fields == null) throw new ArgumentNullException(nameof(fields)); 153 | return OrderBy(CreateSqlFields(alias, fields)); 154 | } 155 | 156 | //------------------------------------------------------------------------- 157 | 158 | public SqlSelect InnerJoin(Expression> condition, 159 | SqlAlias leftAlias = null, SqlAlias joinAlias = null) 160 | { 161 | if (condition == null) throw new ArgumentNullException(nameof(condition)); 162 | if (leftAlias == null) 163 | leftAlias = MetadataProvider.AliasFor(); 164 | if (joinAlias == null) 165 | joinAlias = MetadataProvider.AliasFor(); 166 | 167 | return InnerJoin(GetJoinFilter(condition.Body as BinaryExpression, leftAlias, joinAlias), joinAlias); 168 | } 169 | 170 | public SqlSelect InnerJoin(ISqlFilter condition, SqlAlias joinAlias = null) 171 | { 172 | if (condition == null) throw new ArgumentNullException(nameof(condition)); 173 | return CreateSqlSelect(Join(JoinType.Inner, condition, joinAlias)); 174 | } 175 | 176 | public SqlSelect LeftJoin(Expression> condition, 177 | SqlAlias leftAlias = null, SqlAlias joinAlias = null) 178 | { 179 | if (condition == null) throw new ArgumentNullException(nameof(condition)); 180 | if (leftAlias == null) 181 | leftAlias = MetadataProvider.AliasFor(); 182 | if (leftAlias == null) 183 | joinAlias = MetadataProvider.AliasFor(); 184 | 185 | return LeftJoin(GetJoinFilter(condition.Body as BinaryExpression, leftAlias, joinAlias), joinAlias); 186 | } 187 | 188 | public SqlSelect LeftJoin(ISqlFilter condition, SqlAlias joinAlias = null) 189 | { 190 | if (condition == null) throw new ArgumentNullException(nameof(condition)); 191 | return CreateSqlSelect(Join(JoinType.Left, condition, joinAlias)); 192 | } 193 | 194 | public SqlSelect RightJoin(Expression> condition, 195 | SqlAlias leftAlias = null, SqlAlias joinAlias = null) 196 | { 197 | if (condition == null) throw new ArgumentNullException(nameof(condition)); 198 | if (leftAlias == null) 199 | leftAlias = MetadataProvider.AliasFor(); 200 | if (joinAlias == null) 201 | joinAlias = MetadataProvider.AliasFor(); 202 | 203 | return RightJoin(GetJoinFilter(condition.Body as BinaryExpression, leftAlias, joinAlias), joinAlias); 204 | } 205 | 206 | public SqlSelect RightJoin(ISqlFilter condition, SqlAlias joinAlias = null) 207 | { 208 | if (condition == null) throw new ArgumentNullException(nameof(condition)); 209 | return CreateSqlSelect(Join(JoinType.Right, condition, joinAlias)); 210 | } 211 | 212 | public SqlSelect FullJoin(Expression> condition, 213 | SqlAlias leftAlias = null, SqlAlias joinAlias = null) 214 | { 215 | if (condition == null) throw new ArgumentNullException(nameof(condition)); 216 | if (leftAlias == null) 217 | leftAlias = MetadataProvider.AliasFor(); 218 | if (joinAlias == null) 219 | joinAlias = MetadataProvider.AliasFor(); 220 | 221 | return FullJoin(GetJoinFilter(condition.Body as BinaryExpression, leftAlias, joinAlias), joinAlias); 222 | } 223 | 224 | public SqlSelect FullJoin(ISqlFilter condition, SqlAlias joinAlias = null) 225 | { 226 | if (condition == null) throw new ArgumentNullException(nameof(condition)); 227 | return CreateSqlSelect(Join(JoinType.Full, condition, joinAlias)); 228 | } 229 | 230 | ISqlSelect ISqlSelect.Join(JoinType joinType, ISqlFilter condition, SqlAlias joinAlias) 231 | { 232 | ISqlSelect result; 233 | switch (joinType) 234 | { 235 | case JoinType.Inner: 236 | result = InnerJoin(condition, joinAlias); 237 | break; 238 | case JoinType.Left: 239 | result = LeftJoin(condition, joinAlias); 240 | break; 241 | case JoinType.Right: 242 | result = RightJoin(condition, joinAlias); 243 | break; 244 | case JoinType.Full: 245 | result = FullJoin(condition, joinAlias); 246 | break; 247 | default: 248 | throw new NotSupportedException($"{joinType.ToString()} is not supported"); 249 | } 250 | return result; 251 | } 252 | 253 | //------------------------------------------------------------------------- 254 | 255 | ISqlSelect ISqlSelect.Where(ISqlFilter filter) => Where(filter); 256 | public SqlSelect Where(ISqlFilter filter) 257 | { 258 | return CreateSqlSelect(Info.Where(filter.WithParameterPrefix("w"))); 259 | } 260 | 261 | ISqlSelect ISqlSelect.Having(ISqlFilter filter) => Having(filter); 262 | public SqlSelect Having(ISqlFilter filter) 263 | { 264 | return CreateSqlSelect(Info.Having(filter.WithParameterPrefix("h"))); 265 | } 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /LambdaSql/SqlSelectBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Data.Common; 4 | using System.Linq; 5 | using System.Linq.Expressions; 6 | using LambdaSql.Field; 7 | using LambdaSql.Filter; 8 | using LambdaSql.QueryBuilder; 9 | 10 | namespace LambdaSql 11 | { 12 | public abstract class SqlSelectBase 13 | { 14 | protected const string SEPARATOR = "\r\n"; 15 | protected const string SEPARATOR_WITH_OFFSET = "\r\n "; 16 | 17 | protected readonly IMetadataProvider MetadataProvider = LambdaSql.MetadataProvider.Instance; 18 | 19 | protected SqlSelectBase(SqlSelectInfo info, ISqlSelectQueryBuilder queryBuilder) 20 | { 21 | Info = info; 22 | QueryBuilder = queryBuilder; 23 | } 24 | 25 | public SqlSelectInfo Info { get; } 26 | protected ISqlSelectQueryBuilder QueryBuilder { get; } 27 | 28 | private string _rawSql; 29 | public string RawSql => _rawSql ?? (_rawSql = QueryBuilder.Build(Info, parametric: false)); 30 | 31 | private string _parametricSql; 32 | public string ParametricSql => _parametricSql ?? (_parametricSql = QueryBuilder.Build(Info, parametric: true)); 33 | 34 | private ISqlField CreateSqlField(string name, ISqlAlias alias) 35 | { 36 | if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException(nameof(name)); 37 | if (alias == null) throw new ArgumentNullException(nameof(alias)); 38 | return new SqlField 39 | { 40 | Name = name, 41 | Alias = alias 42 | }; 43 | } 44 | 45 | protected ISqlField[] CreateSqlFields(ISqlAlias alias, IEnumerable>> fields) 46 | { 47 | return fields.Select(f => CreateSqlField(MetadataProvider.GetPropertyName(f), alias)).ToArray(); 48 | } 49 | 50 | protected ISqlFilter GetJoinFilter(BinaryExpression expression, SqlAlias leftAlias, SqlAlias joinAlias) 51 | { 52 | if (leftAlias == null) throw new ArgumentNullException(nameof(leftAlias)); 53 | if (joinAlias == null) throw new ArgumentNullException(nameof(joinAlias)); 54 | if (expression == null || expression.NodeType != ExpressionType.Equal) 55 | throw new JoinException("Invalid join expression"); 56 | 57 | var leftExpr = LibHelper.GetMemberExpression(expression.Left); 58 | var rightExpr = LibHelper.GetMemberExpression(expression.Right); 59 | 60 | if (leftExpr == null) throw new ArgumentNullException(nameof(leftExpr)); 61 | if (rightExpr == null) throw new ArgumentNullException(nameof(rightExpr)); 62 | 63 | var rightField = leftExpr.Expression.Type == leftAlias.EntityType 64 | ? CreateSqlField(MetadataProvider.GetPropertyName(rightExpr), joinAlias) 65 | : (ISqlField)CreateSqlField(MetadataProvider.GetPropertyName(rightExpr), leftAlias); 66 | 67 | return leftExpr.Expression.Type == leftAlias.EntityType 68 | ? SqlFilter.From(SqlField.From(typeof(TLeft), typeof(int), 69 | leftAlias, MetadataProvider.GetPropertyName(leftExpr))).EqualTo(rightField) 70 | : (ISqlFilter)SqlFilter.From(SqlField.From(typeof(TJoin), typeof(int), 71 | joinAlias, MetadataProvider.GetPropertyName(leftExpr))).EqualTo(rightField); 72 | } 73 | 74 | protected SqlSelectInfo Join(JoinType joinType, ISqlFilter condition, SqlAlias joinAlias = null) 75 | { 76 | if (condition == null) throw new ArgumentNullException(nameof(condition)); 77 | if (joinAlias == null) 78 | joinAlias = MetadataProvider.AliasFor(); 79 | if (Info.Joins().Any(j => j.JoinAlias.Value == joinAlias.Value)) 80 | throw new JoinException($"Alias '{joinAlias.Value}' is already registered"); 81 | 82 | var join = new SqlJoin(joinType, condition, joinAlias); 83 | return Info.Joins(join); 84 | } 85 | 86 | protected DbParameter[] GetFilterParameters(SqlSelectInfo info) 87 | => Info.Where()?.Parameters? 88 | .Concat(Info.Having()?.Parameters ?? new DbParameter[0]).ToArray() 89 | ?? Info.Having()?.Parameters 90 | ?? new DbParameter[0]; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /LambdaSql/SqlSelectInfo.cs: -------------------------------------------------------------------------------- 1 | using LambdaSql.Field; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Collections.Immutable; 5 | using System.Linq; 6 | using LambdaSql.Filter; 7 | 8 | namespace LambdaSql 9 | { 10 | public class SqlSelectInfo : ICloneable 11 | { 12 | public SqlSelectInfo(ISqlAlias alias) 13 | { 14 | Alias = alias ?? throw new ArgumentNullException(nameof(alias)); 15 | } 16 | 17 | public ISqlAlias Alias { get; } 18 | 19 | private IReadOnlyList _allAliases; 20 | public IReadOnlyList AllAliases => _allAliases ?? (_allAliases = 21 | ImmutableList.CreateRange(new[] {Alias}.Concat(Joins() 22 | .Select(j => j.JoinAlias)))); 23 | 24 | object ICloneable.Clone() 25 | { 26 | return MemberwiseClone(); 27 | } 28 | 29 | public SqlSelectInfo Clone() 30 | { 31 | return (SqlSelectInfo)MemberwiseClone(); 32 | } 33 | 34 | private int? _top; 35 | public int? Top() => _top; 36 | public SqlSelectInfo Top(int? value) 37 | { 38 | var clone = Clone(); 39 | clone._top = value; 40 | return clone; 41 | } 42 | 43 | private bool _distinct; 44 | public bool Distinct() => _distinct; 45 | public SqlSelectInfo Distinct(bool value) 46 | { 47 | var clone = Clone(); 48 | clone._distinct = value; 49 | return clone; 50 | } 51 | 52 | private ImmutableList _selectFields = ImmutableList.Empty; 53 | public ImmutableList SelectFields() => _selectFields; 54 | public SqlSelectInfo SelectFields(params ISqlField[] fields) 55 | { 56 | var clone = Clone(); 57 | clone._selectFields = clone._selectFields.AddRange(fields); 58 | return clone; 59 | } 60 | 61 | private ImmutableList _groupByFields = ImmutableList.Empty; 62 | public ImmutableList GroupByFields() => _groupByFields; 63 | public SqlSelectInfo GroupByFields(params ISqlField[] fields) 64 | { 65 | var clone = Clone(); 66 | clone._groupByFields = clone._groupByFields.AddRange(fields); 67 | return clone; 68 | } 69 | 70 | private ImmutableList _orderByFields = ImmutableList.Empty; 71 | public ImmutableList OrderByFields() => _orderByFields; 72 | public SqlSelectInfo OrderByFields(params ISqlField[] fields) 73 | { 74 | var clone = Clone(); 75 | clone._orderByFields = clone._orderByFields.AddRange(fields); 76 | return clone; 77 | } 78 | 79 | private ImmutableList _joins = ImmutableList.Empty; 80 | public ImmutableList Joins() => _joins; 81 | public SqlSelectInfo Joins(params ISqlJoin[] joins) 82 | { 83 | var clone = Clone(); 84 | clone._joins = clone._joins.AddRange(joins); 85 | return clone; 86 | } 87 | 88 | private ISqlFilter _where; 89 | public ISqlFilter Where() => _where; 90 | public SqlSelectInfo Where(ISqlFilter value) 91 | { 92 | var clone = Clone(); 93 | clone._where = value; 94 | return clone; 95 | } 96 | 97 | private ISqlFilter _having; 98 | public ISqlFilter Having() => _having; 99 | public SqlSelectInfo Having(ISqlFilter value) 100 | { 101 | var clone = Clone(); 102 | clone._having = value; 103 | return clone; 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /LambdaSql/SqlSelectWrapper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Data.Common; 3 | using System.Linq; 4 | using System.Linq.Expressions; 5 | using LambdaSql.Field; 6 | using LambdaSql.Filter; 7 | using LambdaSql.QueryBuilder; 8 | 9 | namespace LambdaSql 10 | { 11 | public class SqlSelect : SqlSelectBase, ISqlSelect 12 | { 13 | private readonly ISqlSelect _innerSqlSelect; 14 | 15 | public SqlSelect(ISqlSelect innerSqlSelect, ISqlAlias alias) 16 | : this(innerSqlSelect, new SqlSelectInfo(alias), 17 | new SqlSelectWrapperQueryBuilder(innerSqlSelect)) 18 | { 19 | } 20 | 21 | private SqlSelect(ISqlSelect innerSqlSelect, SqlSelectInfo info, ISqlSelectQueryBuilder queryBuilder) : base(info, queryBuilder) 22 | { 23 | _innerSqlSelect = innerSqlSelect ?? throw new ArgumentNullException(nameof(innerSqlSelect)); 24 | } 25 | 26 | private SqlSelect CreateSqlSelect(SqlSelectInfo info) => new SqlSelect(_innerSqlSelect, info, QueryBuilder); 27 | 28 | ISqlSelect ISqlSelect.Extend(Func decorationCallback) => Extend(decorationCallback); 29 | public SqlSelect Extend(Func decorationCallback) 30 | => new SqlSelect(_innerSqlSelect, Info, decorationCallback(QueryBuilder)); 31 | 32 | private DbParameter[] _parameters; 33 | public DbParameter[] Parameters => _parameters 34 | ?? (_parameters = _innerSqlSelect.Parameters.Concat(GetFilterParameters(Info)).ToArray()); 35 | 36 | public override string ToString() => RawSql; 37 | 38 | public Type EntityType => null; 39 | 40 | public SqlSelect Distinct() 41 | { 42 | return Distinct(true); 43 | } 44 | 45 | ISqlSelect ISqlSelect.Distinct(bool isDistinct) => Distinct(isDistinct); 46 | public SqlSelect Distinct(bool isDistinct) 47 | { 48 | return CreateSqlSelect(Info.Distinct(isDistinct)); 49 | } 50 | 51 | //------------------------------------------------------------------------- 52 | 53 | ISqlSelect ISqlSelect.AddFields(ISqlField[] fields) => AddFields(fields); 54 | public SqlSelect AddFields(params ISqlField[] fields) 55 | { 56 | if (fields == null) throw new ArgumentNullException(nameof(fields)); 57 | return CreateSqlSelect(Info.SelectFields(fields.Select(f => 58 | { 59 | f.Alias = Info.Alias; 60 | return f; 61 | }).ToArray())); 62 | } 63 | 64 | public SqlSelect AddFields(params Expression>[] fields) 65 | { 66 | if (fields == null) throw new ArgumentNullException(nameof(fields)); 67 | return AddFields(CreateSqlFields(Info.Alias, fields)); 68 | } 69 | 70 | ISqlSelect ISqlSelect.AddFields(SqlAlias alias, params Expression>[] fields) 71 | => AddFields(fields); 72 | 73 | //------------------------------------------------------------------------- 74 | 75 | ISqlSelect ISqlSelect.GroupBy(ISqlField[] fields) => GroupBy(fields); 76 | public SqlSelect GroupBy(params ISqlField[] fields) 77 | { 78 | if (fields == null) throw new ArgumentNullException(nameof(fields)); 79 | return CreateSqlSelect(Info.GroupByFields(fields.Select(f => 80 | { 81 | f.Alias = Info.Alias; 82 | return f; 83 | }).ToArray())); 84 | } 85 | 86 | public SqlSelect GroupBy(params Expression>[] fields) 87 | { 88 | if (fields == null) throw new ArgumentNullException(nameof(fields)); 89 | return GroupBy(CreateSqlFields(Info.Alias, fields)); 90 | } 91 | 92 | ISqlSelect ISqlSelect.GroupBy(SqlAlias alias, params Expression>[] fields) 93 | => GroupBy(fields); 94 | 95 | //------------------------------------------------------------------------- 96 | 97 | ISqlSelect ISqlSelect.OrderBy(ISqlField[] fields) => OrderBy(fields); 98 | public SqlSelect OrderBy(params ISqlField[] fields) 99 | { 100 | if (fields == null) throw new ArgumentNullException(nameof(fields)); 101 | return CreateSqlSelect(Info.OrderByFields(fields.Select(f => 102 | { 103 | f.Alias = Info.Alias; 104 | return f; 105 | }).ToArray())); 106 | } 107 | 108 | public SqlSelect OrderBy(params Expression>[] fields) 109 | { 110 | if (fields == null) throw new ArgumentNullException(nameof(fields)); 111 | return OrderBy(CreateSqlFields(Info.Alias, fields)); 112 | } 113 | 114 | ISqlSelect ISqlSelect.OrderBy(SqlAlias alias, params Expression>[] fields) 115 | => OrderBy(fields); 116 | 117 | //------------------------------------------------------------------------- 118 | 119 | public SqlSelect InnerJoin(Expression> condition, 120 | SqlAlias leftAlias = null, SqlAlias joinAlias = null) 121 | { 122 | if (condition == null) throw new ArgumentNullException(nameof(condition)); 123 | if (leftAlias == null) 124 | leftAlias = MetadataProvider.AliasFor(); 125 | if (joinAlias == null) 126 | joinAlias = MetadataProvider.AliasFor(); 127 | 128 | return InnerJoin(GetJoinFilter(condition.Body as BinaryExpression, leftAlias, joinAlias), joinAlias); 129 | } 130 | 131 | public SqlSelect InnerJoin(ISqlFilter condition, SqlAlias joinAlias = null) 132 | { 133 | if (condition == null) throw new ArgumentNullException(nameof(condition)); 134 | return CreateSqlSelect(Join(JoinType.Inner, condition, joinAlias)); 135 | } 136 | 137 | public SqlSelect LeftJoin(Expression> condition, 138 | SqlAlias leftAlias = null, SqlAlias joinAlias = null) 139 | { 140 | if (condition == null) throw new ArgumentNullException(nameof(condition)); 141 | if (leftAlias == null) 142 | leftAlias = MetadataProvider.AliasFor(); 143 | if (leftAlias == null) 144 | joinAlias = MetadataProvider.AliasFor(); 145 | 146 | return LeftJoin(GetJoinFilter(condition.Body as BinaryExpression, leftAlias, joinAlias), joinAlias); 147 | } 148 | 149 | public SqlSelect LeftJoin(ISqlFilter condition, SqlAlias joinAlias = null) 150 | { 151 | if (condition == null) throw new ArgumentNullException(nameof(condition)); 152 | return CreateSqlSelect(Join(JoinType.Left, condition, joinAlias)); 153 | } 154 | 155 | public SqlSelect RightJoin(Expression> condition, 156 | SqlAlias leftAlias = null, SqlAlias joinAlias = null) 157 | { 158 | if (condition == null) throw new ArgumentNullException(nameof(condition)); 159 | if (leftAlias == null) 160 | leftAlias = MetadataProvider.AliasFor(); 161 | if (joinAlias == null) 162 | joinAlias = MetadataProvider.AliasFor(); 163 | 164 | return RightJoin(GetJoinFilter(condition.Body as BinaryExpression, leftAlias, joinAlias), joinAlias); 165 | } 166 | 167 | public SqlSelect RightJoin(ISqlFilter condition, SqlAlias joinAlias = null) 168 | { 169 | if (condition == null) throw new ArgumentNullException(nameof(condition)); 170 | return CreateSqlSelect(Join(JoinType.Right, condition, joinAlias)); 171 | } 172 | 173 | public SqlSelect FullJoin(Expression> condition, 174 | SqlAlias leftAlias = null, SqlAlias joinAlias = null) 175 | { 176 | if (condition == null) throw new ArgumentNullException(nameof(condition)); 177 | if (leftAlias == null) 178 | leftAlias = MetadataProvider.AliasFor(); 179 | if (joinAlias == null) 180 | joinAlias = MetadataProvider.AliasFor(); 181 | 182 | return FullJoin(GetJoinFilter(condition.Body as BinaryExpression, leftAlias, joinAlias), joinAlias); 183 | } 184 | 185 | public SqlSelect FullJoin(ISqlFilter condition, SqlAlias joinAlias = null) 186 | { 187 | if (condition == null) throw new ArgumentNullException(nameof(condition)); 188 | return CreateSqlSelect(Join(JoinType.Full, condition, joinAlias)); 189 | } 190 | 191 | ISqlSelect ISqlSelect.Join(JoinType joinType, ISqlFilter condition, SqlAlias joinAlias) 192 | { 193 | ISqlSelect result; 194 | switch (joinType) 195 | { 196 | case JoinType.Inner: 197 | result = InnerJoin(condition, joinAlias); 198 | break; 199 | case JoinType.Left: 200 | result = LeftJoin(condition, joinAlias); 201 | break; 202 | case JoinType.Right: 203 | result = RightJoin(condition, joinAlias); 204 | break; 205 | case JoinType.Full: 206 | result = FullJoin(condition, joinAlias); 207 | break; 208 | default: 209 | throw new NotSupportedException($"{joinType.ToString()} is not supported"); 210 | } 211 | return result; 212 | } 213 | 214 | //------------------------------------------------------------------------- 215 | 216 | ISqlSelect ISqlSelect.Where(ISqlFilter filter) => Where(filter); 217 | public SqlSelect Where(ISqlFilter filter) 218 | { 219 | if (filter == null) throw new ArgumentNullException(nameof(filter)); 220 | return CreateSqlSelect(Info.Where(filter.WithParameterPrefix($"{Info.Alias.Value}_w"))); 221 | } 222 | 223 | ISqlSelect ISqlSelect.Having(ISqlFilter filter) => Having(filter); 224 | public SqlSelect Having(ISqlFilter filter) 225 | { 226 | if (filter == null) throw new ArgumentNullException(nameof(filter)); 227 | return CreateSqlSelect(Info.Having(filter.WithParameterPrefix($"{Info.Alias.Value}_h"))); 228 | } 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LambdaSql 2 | 3 | [![Build status](https://ci.appveyor.com/api/projects/status/6bl8g1j9ej57nhje?svg=true)](https://ci.appveyor.com/project/Serg046/lambdasql) [![NuGet version](https://badge.fury.io/nu/LambdaSql.svg)](https://badge.fury.io/nu/LambdaSql) 4 | 5 | Provides possibility to create sql query based on entity type and build query using OOP style. 6 | It uses a member lambda expression to get column name and has corresponding methods for each sql clause. 7 | 8 | ## Structure 9 | The main two types are SqlSelect and SqlFilter. 10 | SqlSelect contains logic to create `select` sql query and SqlFilter is used for some filter like `where` or `having` clauses. 11 | All features are **immutable**. 12 | 13 | - SqlSelect contract 14 | ```csharp 15 | SqlSelect Extend(Func decorationCallback); 16 | Type EntityType { get; } 17 | string RawSql { get; } 18 | string ParametricSql { get; } 19 | DbParameter[] Parameters { get; } 20 | SqlSelect Distinct(); 21 | SqlSelect AddFields(params Expression>[] fields); 22 | SqlSelect GroupBy(params Expression>[] fields); 23 | SqlSelect OrderBy(params Expression>[] fields); 24 | SqlSelect InnerJoin(...); 25 | SqlSelect LeftJoin(...); 26 | SqlSelect RightJoin(...); 27 | SqlSelect FullJoin(...); 28 | SqlSelect Where(ISqlFilter filter); 29 | SqlSelect Having(ISqlFilter filter); 30 | ``` 31 | - SqlFilter contract 32 | ```csharp 33 | string RawSql { get; } 34 | SqlParameter[] Parameters { get; } 35 | string ParametricSql { get; } 36 | SqlFilterField And(...); 37 | SqlFilter And(SqlFilter filter); 38 | SqlFilterField Or(...); 39 | SqlFilter Or(SqlFilter filter); 40 | SqlFilter AndGroup(SqlFilter filter); 41 | SqlFilter OrGroup(SqlFilter filter); 42 | ``` 43 | - SqlFilterField contract 44 | ```csharp 45 | TResult SatisfyLambda(Func filter); 46 | TResult IsNull(); 47 | TResult IsNotNull(); 48 | TResult Like(string value); 49 | TResult EqualTo(TFieldType value); 50 | TResult EqualTo(Expression> field); 51 | TResult NotEqualTo(TFieldType value); 52 | TResult NotEqualTo(Expression> field); 53 | TResult GreaterThan(TFieldType value); 54 | TResult GreaterThan(Expression> field); 55 | TResult GreaterThanOrEqual(TFieldType value); 56 | TResult GreaterThanOrEqual(Expression> field); 57 | TResult LessThan(TFieldType value); 58 | TResult LessThan(Expression> field); 59 | TResult LessThanOrEqual(TFieldType value); 60 | TResult LessThanOrEqual(Expression> field); 61 | TResult In(params TFieldType[] values); 62 | TResult NotIn(params TFieldType[] values); 63 | ``` 64 | 65 | ## Usage 66 | - Entity types 67 | ```csharp 68 | public class Person 69 | { 70 | public int Id { get; } 71 | public string Name { get; } 72 | public int PassportId { get; } 73 | } 74 | 75 | public class Passport 76 | { 77 | public int Id { get; } 78 | public string Number { get; } 79 | } 80 | ``` 81 | - Simple query 82 | ```csharp 83 | var qry = new SqlSelect() 84 | .AddFields(p => p.Id) 85 | .Where(SqlFilter.From(p => p.Name).EqualTo("Sergey")); 86 | 87 | Console.WriteLine(qry.ParametricSql); 88 | Console.WriteLine("---"); 89 | Console.WriteLine(string.Join("; ", qry.Parameters 90 | .Select(p => $"Name = {p.ParameterName}, Value = {p.Value}"))); 91 | ``` 92 | ```sql 93 | SELECT 94 | pe.Id 95 | FROM 96 | Person pe 97 | WHERE 98 | pe.Name = @w0 99 | --- 100 | Name = @w0, Value = Sergey 101 | ``` 102 | - Group by and having clauses 103 | ```csharp 104 | var qry = new SqlSelect() 105 | .AddFields(p => p.Name) 106 | .AddFields(SqlField.Count(p => p.Name)) 107 | .Where(SqlFilter.From(p => p.Id).GreaterThan(5)) 108 | .GroupBy(p => p.Name) 109 | .Having(SqlFilter.From(p => p.Name).EqualTo("Sergey")); 110 | 111 | Console.WriteLine(qry.ParametricSql); 112 | Console.WriteLine("---"); 113 | Console.WriteLine(string.Join("; ", qry.Parameters 114 | .Select(p => $"Name = {p.ParameterName}, Value = {p.Value}"))); 115 | ``` 116 | ```sql 117 | SELECT 118 | pe.Name, COUNT(pe.Name) 119 | FROM 120 | Person pe 121 | WHERE 122 | pe.Id > @w0 123 | GROUP BY 124 | pe.Name 125 | HAVING 126 | pe.Name = @h0 127 | --- 128 | Name = @w0, Value = 5; Name = @h0, Value = Sergey 129 | ``` 130 | - Nested query 131 | ```csharp 132 | var qry = new SqlSelect 133 | ( 134 | new SqlSelect() 135 | .AddFields(p => p.Id, p => p.Name) 136 | .Where(SqlFilter.From(p => p.Name).EqualTo("Sergey")) 137 | , new SqlAlias("inner") 138 | ).AddFields(p => p.Name); 139 | 140 | Console.WriteLine(qry.ParametricSql); 141 | Console.WriteLine("---"); 142 | Console.WriteLine(string.Join("; ", qry.Parameters 143 | .Select(p => $"Name = {p.ParameterName}, Value = {p.Value}"))); 144 | ``` 145 | ```sql 146 | SELECT 147 | inner.Name 148 | FROM 149 | ( 150 | SELECT 151 | pe.Id, pe.Name 152 | FROM 153 | Person pe 154 | WHERE 155 | pe.Name = @w0 156 | ) AS inner 157 | --- 158 | Name = @w0, Value = Sergey 159 | ``` 160 | - Inner join 161 | ```csharp 162 | var joinByLambda = new SqlSelect() 163 | .InnerJoin((person, passport) => person.PassportId == passport.Id); 164 | var joinByFilter = new SqlSelect() 165 | .InnerJoin(SqlFilter.From(p => p.Id).EqualTo(p => p.PassportId)); 166 | 167 | Console.WriteLine(joinByLambda.ParametricSql); 168 | Console.WriteLine("---"); 169 | Console.WriteLine(joinByFilter.ParametricSql); 170 | ``` 171 | ```sql 172 | SELECT 173 | * 174 | FROM 175 | Person pe 176 | INNER JOIN 177 | Passport pa ON pe.PassportId = pa.Id 178 | --- 179 | SELECT 180 | * 181 | FROM 182 | Person pe 183 | INNER JOIN 184 | Passport pa ON pa.Id = pe.PassportId 185 | ``` 186 | ## Extensibility 187 | - SqlSelectQueryBuilder contract 188 | ```csharp 189 | ISqlSelectQueryBuilder ModifySelectFields(ModifyQueryPartCallback modificationCallback); 190 | ISqlSelectQueryBuilder ModifyJoins(ModifyQueryPartCallback modificationCallback); 191 | ISqlSelectQueryBuilder ModifyWhereFilters(ModifyQueryPartCallback modificationCallback); 192 | ISqlSelectQueryBuilder ModifyGroupByFields(ModifyQueryPartCallback modificationCallback); 193 | ISqlSelectQueryBuilder ModifyHavingFilters(ModifyQueryPartCallback modificationCallback); 194 | ISqlSelectQueryBuilder ModifyOrderByFields(ModifyQueryPartCallback modificationCallback); 195 | ISqlSelectQueryBuilder Modify(ModifyQueryPartCallback modificationCallback); //To modify the whole query 196 | ``` 197 | - MySql Limit 198 | ```csharp 199 | public static ISqlSelect Limit(this ISqlSelect select, int count, int? offset = null) 200 | { 201 | if (count <= 0) 202 | { 203 | throw new ArgumentException("Parameter \"count\" must be a possitive number"); 204 | } 205 | if (offset.HasValue && offset <= 0) 206 | { 207 | throw new ArgumentException("Parameter \"offset\" must be a possitive number"); 208 | } 209 | return offset == null 210 | ? select.Extend(queryBuilder => queryBuilder.Modify(query => $"{query}{Environment.NewLine}LIMIT {count}")) 211 | : select.Extend(queryBuilder => queryBuilder.Modify(query => $"{query}{Environment.NewLine}LIMIT {count} OFFSET {offset}")); 212 | } 213 | 214 | static void Main(string[] args) 215 | { 216 | Console.WriteLine(new SqlSelect().Limit(10).RawSql); 217 | Console.WriteLine(); 218 | Console.WriteLine(new SqlSelect().Limit(10, 5).RawSql); 219 | } 220 | ``` 221 | ```sql 222 | SELECT 223 | * 224 | FROM 225 | Person pe 226 | LIMIT 10 227 | 228 | SELECT 229 | * 230 | FROM 231 | Person pe 232 | LIMIT 10 OFFSET 5 233 | ``` 234 | - Oracle ROWNUM 235 | ```csharp 236 | public static ISqlSelect Top(this ISqlSelect select, int count) 237 | { 238 | if (count <= 0) 239 | { 240 | throw new ArgumentException("Parameter \"count\" must be a possitive number"); 241 | } 242 | return select.Extend(queryBuilder => queryBuilder.ModifyWhereFilters(where => where.Length == 0 243 | ? $"{Environment.NewLine}WHERE{Environment.NewLine} ROWNUM <= {count}" 244 | : $"{where} AND ROWNUM <= {count}")); 245 | } 246 | 247 | static void Main(string[] args) 248 | { 249 | Console.WriteLine(new SqlSelect().Top(10).RawSql); 250 | Console.WriteLine(); 251 | Console.WriteLine(new SqlSelect() 252 | .Where(SqlFilter.From(person => person.Name).EqualTo("Sergey")) 253 | .Top(10).RawSql); 254 | } 255 | ``` 256 | ```sql 257 | SELECT 258 | * 259 | FROM 260 | Person pe 261 | WHERE 262 | ROWNUM <= 10 263 | 264 | SELECT 265 | * 266 | FROM 267 | Person pe 268 | WHERE 269 | pe.Name = 'Sergey' AND ROWNUM <= 10 270 | ``` 271 | --------------------------------------------------------------------------------