├── .gitattributes ├── .gitignore ├── Dapper.GraphQL.Test ├── Dapper.GraphQL.Test.csproj ├── EntityMapContextTests.cs ├── EntityMappers │ ├── CompanyEntityMapper.cs │ └── PersonEntityMapper.cs ├── GraphQL │ ├── CompanyType.cs │ ├── EmailType.cs │ ├── GraphQlQuery.cs │ ├── PersonInputType.cs │ ├── PersonMutation.cs │ ├── PersonQuery.cs │ ├── PersonSchema.cs │ ├── PersonType.cs │ └── PhoneType.cs ├── GraphQLTests.cs ├── InsertTests.cs ├── Models │ ├── Company.cs │ ├── Email.cs │ ├── Person.cs │ └── Phone.cs ├── QueryBuilders │ ├── CompanyQueryBuilder.cs │ ├── EmailQueryBuilder.cs │ ├── PersonQueryBuilder.cs │ └── PhoneQueryBuilder.cs ├── QueryTests.cs ├── Sql │ ├── 1-Create.sql │ └── 2-Data.sql ├── TestFixture.cs └── UpdateTests.cs ├── Dapper.GraphQL.sln ├── Dapper.GraphQL ├── Contexts │ ├── EntityMapContext.cs │ ├── SqlDeleteContext.cs │ ├── SqlInsertContext.cs │ ├── SqlQueryContext.cs │ └── SqlUpdateContext.cs ├── Dapper.GraphQL.csproj ├── DapperGraphQLOptions.cs ├── DeduplicatingEntityMapper.cs ├── EntityMapper.cs ├── Extensions │ ├── EntityMapContextExtensions.cs │ ├── IEnumerable`Extensions.cs │ ├── IHaveSelectionSetExtensions.cs │ ├── PostgreSql.cs │ ├── ServiceCollectionExtensions.cs │ └── SqlInsertContextExtensions.cs ├── Interfaces │ ├── IEntityMapper.cs │ └── IQueryBuilder.cs ├── ParameterHelper.cs ├── Properties │ └── PublishProfiles │ │ └── FolderProfile.pubxml ├── SqlBuilder.cs └── SqlOptions.cs ├── LICENSE └── 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 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | project.fragment.lock.json 46 | artifacts/ 47 | 48 | *_i.c 49 | *_p.c 50 | *_i.h 51 | *.ilk 52 | *.meta 53 | *.obj 54 | *.pch 55 | *.pdb 56 | *.pgc 57 | *.pgd 58 | *.rsp 59 | *.sbr 60 | *.tlb 61 | *.tli 62 | *.tlh 63 | *.tmp 64 | *.tmp_proj 65 | *.log 66 | *.vspscc 67 | *.vssscc 68 | .builds 69 | *.pidb 70 | *.svclog 71 | *.scc 72 | 73 | # Chutzpah Test files 74 | _Chutzpah* 75 | 76 | # Visual C++ cache files 77 | ipch/ 78 | *.aps 79 | *.ncb 80 | *.opendb 81 | *.opensdf 82 | *.sdf 83 | *.cachefile 84 | *.VC.db 85 | *.VC.VC.opendb 86 | 87 | # Visual Studio profiler 88 | *.psess 89 | *.vsp 90 | *.vspx 91 | *.sap 92 | 93 | # TFS 2012 Local Workspace 94 | $tf/ 95 | 96 | # Guidance Automation Toolkit 97 | *.gpState 98 | 99 | # ReSharper is a .NET coding add-in 100 | _ReSharper*/ 101 | *.[Rr]e[Ss]harper 102 | *.DotSettings.user 103 | 104 | # JustCode is a .NET coding add-in 105 | .JustCode 106 | 107 | # TeamCity is a build add-in 108 | _TeamCity* 109 | 110 | # DotCover is a Code Coverage Tool 111 | *.dotCover 112 | 113 | # NCrunch 114 | _NCrunch_* 115 | .*crunch*.local.xml 116 | nCrunchTemp_* 117 | 118 | # MightyMoose 119 | *.mm.* 120 | AutoTest.Net/ 121 | 122 | # Web workbench (sass) 123 | .sass-cache/ 124 | 125 | # Installshield output folder 126 | [Ee]xpress/ 127 | 128 | # DocProject is a documentation generator add-in 129 | DocProject/buildhelp/ 130 | DocProject/Help/*.HxT 131 | DocProject/Help/*.HxC 132 | DocProject/Help/*.hhc 133 | DocProject/Help/*.hhk 134 | DocProject/Help/*.hhp 135 | DocProject/Help/Html2 136 | DocProject/Help/html 137 | 138 | # Click-Once directory 139 | publish/ 140 | 141 | # Publish Web Output 142 | *.[Pp]ublish.xml 143 | *.azurePubxml 144 | # TODO: Comment the next line if you want to checkin your web deploy settings 145 | # but database connection strings (with potential passwords) will be unencrypted 146 | #*.pubxml 147 | *.publishproj 148 | 149 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 150 | # checkin your Azure Web App publish settings, but sensitive information contained 151 | # in these scripts will be unencrypted 152 | PublishScripts/ 153 | 154 | # NuGet Packages 155 | *.nupkg 156 | # The packages folder can be ignored because of Package Restore 157 | **/packages/* 158 | # except build/, which is used as an MSBuild target. 159 | !**/packages/build/ 160 | # Uncomment if necessary however generally it will be regenerated when needed 161 | #!**/packages/repositories.config 162 | # NuGet v3's project.json files produces more ignoreable files 163 | *.nuget.props 164 | *.nuget.targets 165 | 166 | # Microsoft Azure Build Output 167 | csx/ 168 | *.build.csdef 169 | 170 | # Microsoft Azure Emulator 171 | ecf/ 172 | rcf/ 173 | 174 | # Windows Store app package directories and files 175 | AppPackages/ 176 | BundleArtifacts/ 177 | Package.StoreAssociation.xml 178 | _pkginfo.txt 179 | 180 | # Visual Studio cache files 181 | # files ending in .cache can be ignored 182 | *.[Cc]ache 183 | # but keep track of directories ending in .cache 184 | !*.[Cc]ache/ 185 | 186 | # Others 187 | ClientBin/ 188 | ~$* 189 | *~ 190 | *.dbmdl 191 | *.dbproj.schemaview 192 | *.jfm 193 | *.pfx 194 | *.publishsettings 195 | node_modules/ 196 | orleans.codegen.cs 197 | 198 | # Since there are multiple workflows, uncomment next line to ignore bower_components 199 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 200 | #bower_components/ 201 | 202 | # RIA/Silverlight projects 203 | Generated_Code/ 204 | 205 | # Backup & report files from converting an old project file 206 | # to a newer Visual Studio version. Backup files are not needed, 207 | # because we have git ;-) 208 | _UpgradeReport_Files/ 209 | Backup*/ 210 | UpgradeLog*.XML 211 | UpgradeLog*.htm 212 | 213 | # SQL Server files 214 | *.mdf 215 | *.ldf 216 | 217 | # Business Intelligence projects 218 | *.rdl.data 219 | *.bim.layout 220 | *.bim_*.settings 221 | 222 | # Microsoft Fakes 223 | FakesAssemblies/ 224 | 225 | # GhostDoc plugin setting file 226 | *.GhostDoc.xml 227 | 228 | # Node.js Tools for Visual Studio 229 | .ntvs_analysis.dat 230 | 231 | # Visual Studio 6 build log 232 | *.plg 233 | 234 | # Visual Studio 6 workspace options file 235 | *.opt 236 | 237 | # Visual Studio LightSwitch build output 238 | **/*.HTMLClient/GeneratedArtifacts 239 | **/*.DesktopClient/GeneratedArtifacts 240 | **/*.DesktopClient/ModelManifest.xml 241 | **/*.Server/GeneratedArtifacts 242 | **/*.Server/ModelManifest.xml 243 | _Pvt_Extensions 244 | 245 | # Paket dependency manager 246 | .paket/paket.exe 247 | paket-files/ 248 | 249 | # FAKE - F# Make 250 | .fake/ 251 | 252 | # JetBrains Rider 253 | .idea/ 254 | *.sln.iml 255 | 256 | # CodeRush 257 | .cr/ 258 | 259 | # Python Tools for Visual Studio (PTVS) 260 | __pycache__/ 261 | *.pyc -------------------------------------------------------------------------------- /Dapper.GraphQL.Test/Dapper.GraphQL.Test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.0 5 | 6 | false 7 | 8 | 0.4.2.0 9 | 10 | 0.4.2.0 11 | 12 | 0.4.2-beta 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | Never 23 | 24 | 25 | Never 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /Dapper.GraphQL.Test/EntityMapContextTests.cs: -------------------------------------------------------------------------------- 1 | using Dapper.GraphQL.Test.EntityMappers; 2 | using Dapper.GraphQL.Test.Models; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Xunit; 6 | 7 | namespace Dapper.GraphQL.Test 8 | { 9 | public class EntityMapContextTests : IClassFixture 10 | { 11 | private readonly TestFixture fixture; 12 | 13 | public EntityMapContextTests(TestFixture fixture) 14 | { 15 | this.fixture = fixture; 16 | } 17 | 18 | [Fact(DisplayName = "EntityMap properly deduplicates")] 19 | public void EntityMapSucceeds() 20 | { 21 | var person1 = new Person 22 | { 23 | FirstName = "Doug", 24 | Id = 2, 25 | LastName = "Day", 26 | MergedToPersonId = 2, 27 | }; 28 | var person2 = new Person 29 | { 30 | FirstName = "Douglas", 31 | Id = 2, 32 | LastName = "Day", 33 | MergedToPersonId = 2, 34 | }; 35 | 36 | var email1 = new Email 37 | { 38 | Address = "dday@landmarkhw.com", 39 | Id = 2, 40 | }; 41 | 42 | var email2 = new Email 43 | { 44 | Address = "dougrday@gmail.com", 45 | Id = 3, 46 | }; 47 | 48 | var phone = new Phone 49 | { 50 | Id = 1, 51 | Number = "8011234567", 52 | Type = PhoneType.Mobile, 53 | }; 54 | 55 | var splitOn = new[] 56 | { 57 | typeof(Person), 58 | typeof(Email), 59 | typeof(Phone), 60 | }; 61 | 62 | var personEntityMapper = new PersonEntityMapper(); 63 | 64 | var graphql = @" 65 | { 66 | query { 67 | firstName 68 | lastName 69 | id 70 | emails { 71 | id 72 | address 73 | } 74 | phones { 75 | id 76 | number 77 | type 78 | } 79 | } 80 | }"; 81 | 82 | var selectionSet = fixture.BuildGraphQLSelection(graphql); 83 | var context1 = new EntityMapContext 84 | { 85 | Items = new object[] 86 | { 87 | person1, 88 | email1, 89 | phone, 90 | }, 91 | SelectionSet = selectionSet, 92 | SplitOn = splitOn, 93 | }; 94 | 95 | person1 = personEntityMapper.Map(context1); 96 | Assert.Equal(3, context1.MappedCount); 97 | 98 | Assert.Equal(2, person1.Id); 99 | Assert.Equal("Doug", person1.FirstName); 100 | Assert.Equal(1, person1.Emails.Count); 101 | Assert.Equal(1, person1.Phones.Count); 102 | 103 | var context2 = new EntityMapContext 104 | { 105 | Items = new object[] 106 | { 107 | person2, 108 | email2, 109 | null, 110 | }, 111 | SelectionSet = selectionSet, 112 | SplitOn = splitOn, 113 | }; 114 | 115 | person2 = personEntityMapper.Map(context2); 116 | Assert.Equal(3, context2.MappedCount); 117 | 118 | // The same reference should have been returned 119 | Assert.Same(person1, person2); 120 | 121 | // A 2nd email was added to person 122 | Assert.Equal(2, person1.Emails.Count); 123 | } 124 | } 125 | } -------------------------------------------------------------------------------- /Dapper.GraphQL.Test/EntityMappers/CompanyEntityMapper.cs: -------------------------------------------------------------------------------- 1 | using Dapper.GraphQL.Test.Models; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | 7 | namespace Dapper.GraphQL.Test.EntityMappers 8 | { 9 | public class CompanyEntityMapper : 10 | DeduplicatingEntityMapper 11 | { 12 | public CompanyEntityMapper() 13 | { 14 | PrimaryKey = c => c.Id; 15 | } 16 | 17 | public override Company Map(EntityMapContext context) 18 | { 19 | // NOTE: Order is very important here. We must map the objects in 20 | // the same order they were queried in the QueryBuilder. 21 | var company = Deduplicate(context.Start()); 22 | var email = context.Next("emails"); 23 | var phone = context.Next("phones"); 24 | 25 | if (company != null) 26 | { 27 | if (email != null && 28 | // Eliminate duplicates 29 | !company.Emails.Any(e => e.Address == email.Address)) 30 | { 31 | company.Emails.Add(email); 32 | } 33 | 34 | if (phone != null && 35 | // Eliminate duplicates 36 | !company.Phones.Any(p => p.Number == phone.Number)) 37 | { 38 | company.Phones.Add(phone); 39 | } 40 | } 41 | 42 | return company; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Dapper.GraphQL.Test/EntityMappers/PersonEntityMapper.cs: -------------------------------------------------------------------------------- 1 | using Dapper.GraphQL; 2 | using Dapper.GraphQL.Test.Models; 3 | using System.Linq; 4 | 5 | namespace Dapper.GraphQL.Test.EntityMappers 6 | { 7 | public class PersonEntityMapper : 8 | DeduplicatingEntityMapper 9 | { 10 | private CompanyEntityMapper companyEntityMapper; 11 | private PersonEntityMapper personEntityMapper; 12 | 13 | public PersonEntityMapper() 14 | { 15 | // Deduplicate entities using MergedToPersonId instead of Id. 16 | PrimaryKey = p => p.MergedToPersonId; 17 | } 18 | 19 | public override Person Map(EntityMapContext context) 20 | { 21 | // Avoid creating the mappers until they're used 22 | // NOTE: this avoids an infinite loop (had these been created in the ctor) 23 | if (companyEntityMapper == null) 24 | { 25 | companyEntityMapper = new CompanyEntityMapper(); 26 | } 27 | if (personEntityMapper == null) 28 | { 29 | personEntityMapper = new PersonEntityMapper(); 30 | } 31 | 32 | // NOTE: Order is very important here. We must map the objects in 33 | // the same order they were queried in the QueryBuilder. 34 | 35 | // Start with the person, and deduplicate 36 | var person = Deduplicate(context.Start()); 37 | var company = context.Next("companies", companyEntityMapper); 38 | var email = context.Next("emails"); 39 | var phone = context.Next("phones"); 40 | var supervisor = context.Next("supervisor", personEntityMapper); 41 | var careerCounselor = context.Next("careerCounselor", personEntityMapper); 42 | 43 | if (person != null) 44 | { 45 | if (company != null && 46 | // Eliminate duplicates 47 | !person.Companies.Any(c => c.Id == company.Id)) 48 | { 49 | person.Companies.Add(company); 50 | } 51 | 52 | if (email != null && 53 | // Eliminate duplicates 54 | !person.Emails.Any(e => e.Address == email.Address)) 55 | { 56 | person.Emails.Add(email); 57 | } 58 | 59 | if (phone != null && 60 | // Eliminate duplicates 61 | !person.Phones.Any(p => p.Number == phone.Number)) 62 | { 63 | person.Phones.Add(phone); 64 | } 65 | 66 | person.Supervisor = person.Supervisor ?? supervisor; 67 | person.CareerCounselor = person.CareerCounselor ?? careerCounselor; 68 | } 69 | 70 | return person; 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /Dapper.GraphQL.Test/GraphQL/CompanyType.cs: -------------------------------------------------------------------------------- 1 | using Dapper.GraphQL.Test.Models; 2 | using GraphQL.Types; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Text; 6 | 7 | namespace Dapper.GraphQL.Test.GraphQL 8 | { 9 | public class CompanyType : 10 | ObjectGraphType 11 | { 12 | public CompanyType() 13 | { 14 | Name = "company"; 15 | Description = "A company."; 16 | 17 | Field( 18 | "id", 19 | description: "A unique identifier for the company.", 20 | resolve: context => context.Source?.Id 21 | ); 22 | 23 | Field( 24 | "name", 25 | description: "The name of the company.", 26 | resolve: context => context.Source?.Name 27 | ); 28 | 29 | Field>( 30 | "emails", 31 | description: "A list of email addresses for the company.", 32 | resolve: context => context.Source?.Emails 33 | ); 34 | 35 | Field>( 36 | "phones", 37 | description: "A list of phone numbers for the company.", 38 | resolve: context => context.Source?.Phones 39 | ); 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /Dapper.GraphQL.Test/GraphQL/EmailType.cs: -------------------------------------------------------------------------------- 1 | using Dapper.GraphQL.Test.Models; 2 | using GraphQL.Types; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Text; 6 | 7 | namespace Dapper.GraphQL.Test.GraphQL 8 | { 9 | public class EmailType : 10 | ObjectGraphType 11 | { 12 | public EmailType() 13 | { 14 | Name = "email"; 15 | Description = "An email address."; 16 | 17 | Field( 18 | "id", 19 | description: "A unique identifier for the email address.", 20 | resolve: context => context.Source?.Id 21 | ); 22 | 23 | Field( 24 | "address", 25 | description: "The email address.", 26 | resolve: context => context.Source?.Address 27 | ); 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /Dapper.GraphQL.Test/GraphQL/GraphQlQuery.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json.Linq; 2 | 3 | namespace Dapper.GraphQL.Test.GraphQL 4 | { 5 | public class GraphQlQuery 6 | { 7 | public string OperationName { get; set; } 8 | public string NamedQuery { get; set; } 9 | public string Query { get; set; } 10 | public JObject Variables { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Dapper.GraphQL.Test/GraphQL/PersonInputType.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Types; 2 | 3 | namespace Dapper.GraphQL.Test.GraphQL 4 | { 5 | public class PersonInputType : InputObjectGraphType 6 | { 7 | public PersonInputType() 8 | { 9 | Name = "PersonInput"; 10 | Field("firstName"); 11 | Field("lastName"); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Dapper.GraphQL.Test/GraphQL/PersonMutation.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Data; 3 | using System.Linq; 4 | using Dapper.GraphQL.Test.EntityMappers; 5 | using Dapper.GraphQL.Test.Models; 6 | using GraphQL.Types; 7 | using Microsoft.Extensions.DependencyInjection; 8 | 9 | 10 | namespace Dapper.GraphQL.Test.GraphQL 11 | { 12 | public class PersonMutation : ObjectGraphType 13 | { 14 | public PersonMutation(IQueryBuilder personQueryBuilder, IServiceProvider serviceProvider) 15 | { 16 | Field( 17 | "addPerson", 18 | description: "Adds new person.", 19 | arguments: new QueryArguments( 20 | new QueryArgument { Name = "person" } 21 | ), 22 | resolve: context => 23 | { 24 | var person = context.GetArgument("person"); 25 | 26 | using (var connection = serviceProvider.GetRequiredService()) 27 | { 28 | person.Id = person.MergedToPersonId = PostgreSql.NextIdentity(connection, (Person p) => p.Id); 29 | 30 | bool success = SqlBuilder 31 | .Insert(person) 32 | .Execute(connection) > 0; 33 | 34 | if (success) 35 | { 36 | var personMapper = new PersonEntityMapper(); 37 | 38 | var query = SqlBuilder 39 | .From("Person") 40 | .Select("FirstName, LastName") 41 | .Where("ID = @personId", new { personId = person.Id }); 42 | 43 | var results = query 44 | .Execute(connection, context.FieldAst, personMapper) 45 | .Distinct(); 46 | return results.FirstOrDefault(); 47 | } 48 | 49 | return null; 50 | } 51 | } 52 | ); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Dapper.GraphQL.Test/GraphQL/PersonQuery.cs: -------------------------------------------------------------------------------- 1 | using Dapper.GraphQL.Test.EntityMappers; 2 | using Dapper.GraphQL.Test.Models; 3 | using GraphQL.Types; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using System; 6 | using System.Data; 7 | using System.Data.Common; 8 | using System.Linq; 9 | 10 | namespace Dapper.GraphQL.Test.GraphQL 11 | { 12 | public class PersonQuery : 13 | ObjectGraphType 14 | { 15 | public PersonQuery( 16 | IQueryBuilder personQueryBuilder, 17 | IServiceProvider serviceProvider) 18 | { 19 | Field>( 20 | "people", 21 | description: "A list of people.", 22 | resolve: context => 23 | { 24 | var alias = "person"; 25 | var query = SqlBuilder 26 | .From(alias) 27 | .OrderBy($"{alias}.Id"); 28 | query = personQueryBuilder.Build(query, context.FieldAst, alias); 29 | 30 | // Create a mapper that understands how to uniquely identify the 'Person' class, 31 | // and will deduplicate people as they pass through it 32 | var personMapper = new PersonEntityMapper(); 33 | 34 | using (var connection = serviceProvider.GetRequiredService()) 35 | { 36 | var results = query 37 | .Execute(connection, context.FieldAst, personMapper) 38 | .Distinct(); 39 | return results; 40 | } 41 | } 42 | ); 43 | 44 | FieldAsync>( 45 | "peopleAsync", 46 | description: "A list of people fetched asynchronously.", 47 | resolve: async context => 48 | { 49 | var alias = "person"; 50 | var query = SqlBuilder 51 | .From($"Person {alias}") 52 | .OrderBy($"{alias}.Id"); 53 | query = personQueryBuilder.Build(query, context.FieldAst, alias); 54 | 55 | // Create a mapper that understands how to uniquely identify the 'Person' class, 56 | // and will deduplicate people as they pass through it 57 | var personMapper = new PersonEntityMapper(); 58 | 59 | using (var connection = serviceProvider.GetRequiredService()) 60 | { 61 | connection.Open(); 62 | 63 | var results = await query.ExecuteAsync(connection, context.FieldAst, personMapper); 64 | return results.Distinct(); 65 | } 66 | } 67 | ); 68 | 69 | Field( 70 | "person", 71 | description: "Gets a person by ID.", 72 | arguments: new QueryArguments( 73 | new QueryArgument { Name = "id", Description = "The ID of the person." } 74 | ), 75 | resolve: context => 76 | { 77 | var id = context.Arguments["id"]; 78 | var alias = "person"; 79 | var query = SqlBuilder 80 | .From($"Person {alias}") 81 | .Where($"{alias}.Id = @id", new { id }) 82 | // Even though we're only getting a single person, the process of deduplication 83 | // may return several entities, so we sort by ID here for consistency 84 | // with test results. 85 | .OrderBy($"{alias}.Id"); 86 | 87 | query = personQueryBuilder.Build(query, context.FieldAst, alias); 88 | 89 | // Create a mapper that understands how to uniquely identify the 'Person' class, 90 | // and will deduplicate people as they pass through it 91 | var personMapper = new PersonEntityMapper(); 92 | 93 | using (var connection = serviceProvider.GetRequiredService()) 94 | { 95 | var results = query 96 | .Execute(connection, context.FieldAst, personMapper) 97 | .Distinct(); 98 | return results.FirstOrDefault(); 99 | } 100 | } 101 | ); 102 | } 103 | } 104 | } -------------------------------------------------------------------------------- /Dapper.GraphQL.Test/GraphQL/PersonSchema.cs: -------------------------------------------------------------------------------- 1 | using GraphQL; 2 | 3 | namespace Dapper.GraphQL.Test.GraphQL 4 | { 5 | public class PersonSchema : 6 | global::GraphQL.Types.Schema 7 | { 8 | public PersonSchema(IDependencyResolver resolver) : base(resolver) 9 | { 10 | Query = resolver.Resolve(); 11 | Mutation = resolver.Resolve(); 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /Dapper.GraphQL.Test/GraphQL/PersonType.cs: -------------------------------------------------------------------------------- 1 | using Dapper.GraphQL.Test.Models; 2 | using GraphQL.Types; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Text; 6 | 7 | namespace Dapper.GraphQL.Test.GraphQL 8 | { 9 | public class PersonType : 10 | ObjectGraphType 11 | { 12 | public PersonType() 13 | { 14 | Name = "person"; 15 | Description = "A person."; 16 | 17 | Field( 18 | "id", 19 | description: "A unique identifier for the person.", 20 | resolve: context => context.Source?.Id 21 | ); 22 | 23 | Field( 24 | "firstName", 25 | description: "The first name of the person.", 26 | resolve: context => context.Source?.FirstName 27 | ); 28 | 29 | Field( 30 | "lastName", 31 | description: "The last name of the person.", 32 | resolve: context => context.Source?.LastName 33 | ); 34 | 35 | Field>( 36 | "companies", 37 | description: "A list of companies for this person.", 38 | resolve: context => context.Source?.Companies 39 | ); 40 | 41 | Field>( 42 | "emails", 43 | description: "A list of email addresses for the person.", 44 | resolve: context => context.Source?.Emails 45 | ); 46 | 47 | Field>( 48 | "phones", 49 | description: "A list of phone numbers for the person.", 50 | resolve: context => context.Source?.Phones 51 | ); 52 | 53 | Field( 54 | "supervisor", 55 | description: "This person's supervisor.", 56 | resolve: context => context.Source?.Supervisor 57 | ); 58 | 59 | Field( 60 | "careerCounselor", 61 | description: "This person's career counselor.", 62 | resolve: context => context.Source?.CareerCounselor 63 | ); 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /Dapper.GraphQL.Test/GraphQL/PhoneType.cs: -------------------------------------------------------------------------------- 1 | using Dapper.GraphQL.Test.Models; 2 | using GraphQL.Types; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Text; 6 | 7 | namespace Dapper.GraphQL.Test.GraphQL 8 | { 9 | public class PhoneType : 10 | ObjectGraphType 11 | { 12 | public PhoneType() 13 | { 14 | Name = "phone"; 15 | Description = "A phone number."; 16 | 17 | Field( 18 | "id", 19 | description: "A unique identifier for the phone number.", 20 | resolve: context => context.Source?.Id 21 | ); 22 | 23 | Field( 24 | "number", 25 | description: "The phone number.", 26 | resolve: context => context.Source?.Number 27 | ); 28 | 29 | Field( 30 | "type", 31 | description: "The type of phone number. One of 'home', 'work', 'mobile', or 'other'.", 32 | resolve: context => context.Source?.Type 33 | ); 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /Dapper.GraphQL.Test/GraphQLTests.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Xunit; 3 | using Dapper.GraphQL.Test.GraphQL; 4 | using Newtonsoft.Json.Linq; 5 | using Dapper.GraphQL.Test.Models; 6 | 7 | namespace Dapper.GraphQL.Test 8 | { 9 | public class GraphQLTests : IClassFixture 10 | { 11 | private readonly TestFixture fixture; 12 | 13 | public GraphQLTests( 14 | TestFixture fixture) 15 | { 16 | this.fixture = fixture; 17 | } 18 | 19 | [Fact(DisplayName = "Full people query should succeed")] 20 | public async Task FullPeopleQuery() 21 | { 22 | var json = await fixture.QueryGraphQLAsync(@" 23 | query { 24 | people { 25 | id 26 | firstName 27 | lastName 28 | emails { 29 | id 30 | address 31 | } 32 | phones { 33 | id 34 | number 35 | type 36 | } 37 | companies { 38 | id 39 | name 40 | } 41 | supervisor { 42 | id 43 | firstName 44 | lastName 45 | emails { 46 | id 47 | address 48 | } 49 | phones { 50 | id 51 | number 52 | type 53 | } 54 | } 55 | careerCounselor { 56 | id 57 | firstName 58 | lastName 59 | emails { 60 | id 61 | address 62 | } 63 | phones { 64 | id 65 | number 66 | type 67 | } 68 | } 69 | } 70 | }"); 71 | 72 | var expectedJson = @" 73 | { 74 | ""data"": { 75 | ""people"": [{ 76 | ""id"": 1, 77 | ""firstName"": ""Hyrum"", 78 | ""lastName"": ""Clyde"", 79 | ""emails"": [{ 80 | ""id"": 1, 81 | ""address"": ""hclyde@landmarkhw.com"" 82 | }], 83 | ""phones"": [], 84 | ""companies"": [{ 85 | ""id"": 1, 86 | ""name"": ""Landmark Home Warranty, LLC"" 87 | }], 88 | ""supervisor"": null, 89 | ""careerCounselor"": null 90 | }, 91 | { 92 | ""id"": 2, 93 | ""firstName"": ""Doug"", 94 | ""lastName"": ""Day"", 95 | ""emails"": [{ 96 | ""id"": 2, 97 | ""address"": ""dday@landmarkhw.com"" 98 | }, 99 | { 100 | ""id"": 3, 101 | ""address"": ""dougrday@gmail.com"" 102 | } 103 | ], 104 | ""phones"": [{ 105 | ""id"": 1, 106 | ""number"": ""8011234567"", 107 | ""type"": 3 108 | }], 109 | ""companies"": [{ 110 | ""id"": 1, 111 | ""name"": ""Landmark Home Warranty, LLC"" 112 | }, 113 | { 114 | ""id"": 2, 115 | ""name"": ""Navitaire, LLC"" 116 | } 117 | ], 118 | ""supervisor"": null, 119 | ""careerCounselor"": { 120 | ""id"": 1, 121 | ""firstName"": ""Hyrum"", 122 | ""lastName"": ""Clyde"", 123 | ""emails"": [{ 124 | ""id"": 1, 125 | ""address"": ""hclyde@landmarkhw.com"" 126 | }], 127 | ""phones"": [] 128 | } 129 | }, 130 | { 131 | ""id"": 3, 132 | ""firstName"": ""Kevin"", 133 | ""lastName"": ""Russon"", 134 | ""emails"": [{ 135 | ""id"": 4, 136 | ""address"": ""krusson@landmarkhw.com"" 137 | }], 138 | ""phones"": [{ 139 | ""id"": 2, 140 | ""number"": ""8019876543"", 141 | ""type"": 3 142 | }, 143 | { 144 | ""id"": 3, 145 | ""number"": ""8011111111"", 146 | ""type"": 1 147 | } 148 | ], 149 | ""companies"": [{ 150 | ""id"": 2, 151 | ""name"": ""Navitaire, LLC"" 152 | }, 153 | { 154 | ""id"": 1, 155 | ""name"": ""Landmark Home Warranty, LLC"" 156 | } 157 | ], 158 | ""supervisor"": { 159 | ""id"": 1, 160 | ""firstName"": ""Hyrum"", 161 | ""lastName"": ""Clyde"", 162 | ""emails"": [{ 163 | ""id"": 1, 164 | ""address"": ""hclyde@landmarkhw.com"" 165 | }], 166 | ""phones"": [] 167 | }, 168 | ""careerCounselor"": { 169 | ""id"": 2, 170 | ""firstName"": ""Doug"", 171 | ""lastName"": ""Day"", 172 | ""emails"": [{ 173 | ""id"": 2, 174 | ""address"": ""dday@landmarkhw.com"" 175 | }, 176 | { 177 | ""id"": 3, 178 | ""address"": ""dougrday@gmail.com"" 179 | } 180 | ], 181 | ""phones"": [{ 182 | ""id"": 1, 183 | ""number"": ""8011234567"", 184 | ""type"": 3 185 | }] 186 | } 187 | } 188 | ] 189 | } 190 | }"; 191 | 192 | Assert.True(fixture.JsonEquals(expectedJson, json)); 193 | } 194 | 195 | [Fact(DisplayName = "Async query should succeed")] 196 | public async Task PeopleAsyncQuery() 197 | { 198 | var json = await fixture.QueryGraphQLAsync(@" 199 | query { 200 | peopleAsync { 201 | id 202 | firstName 203 | lastName 204 | } 205 | }"); 206 | 207 | var expectedJson = @" 208 | { 209 | ""data"": { 210 | ""peopleAsync"": [ 211 | { 212 | ""id"": 1, 213 | ""firstName"": ""Hyrum"", 214 | ""lastName"": ""Clyde"" 215 | }, 216 | { 217 | ""id"": 2, 218 | ""firstName"": ""Doug"", 219 | ""lastName"": ""Day"" 220 | }, 221 | { 222 | ""id"": 3, 223 | ""firstName"": ""Kevin"", 224 | ""lastName"": ""Russon"" 225 | } 226 | ] 227 | } 228 | }"; 229 | 230 | Assert.True(fixture.JsonEquals(expectedJson, json)); 231 | } 232 | 233 | [Fact(DisplayName = "Person query should succeed")] 234 | public async Task PersonQuery() 235 | { 236 | var json = await fixture.QueryGraphQLAsync(@" 237 | query { 238 | person (id: 2) { 239 | id 240 | firstName 241 | lastName 242 | emails { 243 | id 244 | address 245 | } 246 | phones { 247 | id 248 | number 249 | type 250 | } 251 | } 252 | }"); 253 | 254 | var expectedJson = @" 255 | { 256 | data: { 257 | person: { 258 | id: 2, 259 | firstName: 'Doug', 260 | lastName: 'Day', 261 | emails: [{ 262 | id: 2, 263 | address: 'dday@landmarkhw.com' 264 | }, { 265 | id: 3, 266 | address: 'dougrday@gmail.com' 267 | }], 268 | phones: [{ 269 | id: 1, 270 | number: '8011234567', 271 | type: 3 272 | }] 273 | } 274 | } 275 | }"; 276 | 277 | Assert.True(fixture.JsonEquals(expectedJson, json)); 278 | } 279 | 280 | [Fact(DisplayName = "Simple people query should succeed")] 281 | public async Task SimplePeopleQuery() 282 | { 283 | var json = await fixture.QueryGraphQLAsync(@" 284 | query { 285 | people { 286 | firstName 287 | lastName 288 | } 289 | }"); 290 | 291 | var expectedJson = @" 292 | { 293 | data: { 294 | people: [ 295 | { 296 | firstName: 'Hyrum', 297 | lastName: 'Clyde' 298 | }, 299 | { 300 | firstName: 'Doug', 301 | lastName: 'Day' 302 | }, 303 | { 304 | firstName: 'Kevin', 305 | lastName: 'Russon' 306 | } 307 | ] 308 | } 309 | }"; 310 | 311 | Assert.True(fixture.JsonEquals(expectedJson, json)); 312 | } 313 | 314 | [Fact(DisplayName = "Simple person query should succeed")] 315 | public async Task SimplePersonQuery() 316 | { 317 | var json = await fixture.QueryGraphQLAsync(@" 318 | query { 319 | person (id: 2) { 320 | id 321 | firstName 322 | lastName 323 | } 324 | }"); 325 | 326 | var expectedJson = @" 327 | { 328 | data: { 329 | person: { 330 | id: 2, 331 | firstName: 'Douglas', 332 | lastName: 'Day' 333 | } 334 | } 335 | }"; 336 | 337 | Assert.True(fixture.JsonEquals(expectedJson, json)); 338 | } 339 | 340 | [Fact(DisplayName = "Simple person insert should succeed")] 341 | public async Task SimplePersonInsert() 342 | { 343 | GraphQlQuery graphQuery = new GraphQlQuery(); 344 | graphQuery.OperationName = "addPerson"; 345 | graphQuery.Variables = JObject.Parse(@"{""person"":{""firstName"":""Joe"",""lastName"":""Doe""}}"); 346 | 347 | graphQuery.Query = @" 348 | mutation ($person: PersonInput!) { 349 | addPerson(person: $person) { 350 | firstName 351 | lastName 352 | } 353 | }"; 354 | 355 | var json = await fixture.QueryGraphQLAsync(graphQuery); 356 | 357 | var expectedJson = @" 358 | { 359 | data: { 360 | addPerson: { 361 | firstName: 'Joe', 362 | lastName: 'Doe' 363 | } 364 | } 365 | }"; 366 | 367 | Assert.True(fixture.JsonEquals(expectedJson, json)); 368 | } 369 | } 370 | } -------------------------------------------------------------------------------- /Dapper.GraphQL.Test/InsertTests.cs: -------------------------------------------------------------------------------- 1 | using Dapper.GraphQL.Test.EntityMappers; 2 | using Dapper.GraphQL.Test.Models; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Xunit; 6 | 7 | namespace Dapper.GraphQL.Test 8 | { 9 | public class InsertTests : IClassFixture 10 | { 11 | private readonly TestFixture fixture; 12 | 13 | public InsertTests(TestFixture fixture) 14 | { 15 | this.fixture = fixture; 16 | } 17 | 18 | [Fact(DisplayName = "INSERT person succeeds")] 19 | public void InsertPerson() 20 | { 21 | Person person = null; 22 | // Ensure inserting a person works and we get IDs back 23 | int emailId = -1; 24 | int personId = -1; 25 | int phoneId = -1; 26 | 27 | try 28 | { 29 | using (var db = fixture.GetDbConnection()) 30 | { 31 | db.Open(); 32 | 33 | // Get the next identity aggressively, as we need to assign 34 | // it to both Id/MergedToPersonId 35 | personId = PostgreSql.NextIdentity(db, (Person p) => p.Id); 36 | Assert.True(personId > 0); 37 | 38 | person = new Person 39 | { 40 | Id = personId, 41 | FirstName = "Steven", 42 | LastName = "Rollman", 43 | MergedToPersonId = personId, 44 | }; 45 | 46 | int insertedCount; 47 | insertedCount = SqlBuilder 48 | .Insert(person) 49 | .Execute(db); 50 | Assert.Equal(1, insertedCount); 51 | 52 | emailId = PostgreSql.NextIdentity(db, (Email e) => e.Id); 53 | var email = new Email 54 | { 55 | Id = emailId, 56 | Address = "srollman@landmarkhw.com", 57 | }; 58 | 59 | var personEmail = new 60 | { 61 | PersonId = personId, 62 | EmailId = emailId, 63 | }; 64 | 65 | phoneId = PostgreSql.NextIdentity(db, (Phone p) => p.Id); 66 | var phone = new Phone 67 | { 68 | Id = phoneId, 69 | Number = "8011115555", 70 | Type = PhoneType.Mobile, 71 | }; 72 | 73 | var personPhone = new 74 | { 75 | PersonId = personId, 76 | PhoneId = phoneId, 77 | }; 78 | 79 | // Add email and phone number to the person 80 | insertedCount = SqlBuilder 81 | .Insert(email) 82 | .Insert(phone) 83 | .Insert("PersonEmail", personEmail) 84 | .Insert("PersonPhone", personPhone) 85 | .Execute(db); 86 | 87 | // Ensure all were inserted properly 88 | Assert.Equal(4, insertedCount); 89 | 90 | // Build an entity mapper for person 91 | var personMapper = new PersonEntityMapper(); 92 | 93 | // Query the person from the database 94 | var query = SqlBuilder 95 | .From("person") 96 | .LeftJoin("PersonEmail personEmail on person.Id = personEmail.Id") 97 | .LeftJoin("Email email on personEmail.EmailId = email.Id") 98 | .LeftJoin("PersonPhone personPhone on person.Id = personPhone.PersonId") 99 | .LeftJoin("Phone phone on personPhone.PhoneId = phone.Id") 100 | .Select("person.*, email.*, phone.*") 101 | .SplitOn("Id") 102 | .SplitOn("Id") 103 | .SplitOn("Id") 104 | .Where("person.Id = @id", new { id = personId }); 105 | 106 | var graphql = @" 107 | { 108 | person { 109 | firstName 110 | lastName 111 | emails { 112 | id 113 | address 114 | } 115 | phones { 116 | id 117 | number 118 | } 119 | } 120 | }"; 121 | var selection = fixture.BuildGraphQLSelection(graphql); 122 | person = query 123 | .Execute(db, selection, personMapper) 124 | .Single(); 125 | } 126 | 127 | // Ensure all inserted data is present 128 | Assert.NotNull(person); 129 | Assert.Equal(personId, person.Id); 130 | Assert.Equal("Steven", person.FirstName); 131 | Assert.Equal("Rollman", person.LastName); 132 | Assert.Equal(1, person.Emails.Count); 133 | Assert.Equal("srollman@landmarkhw.com", person.Emails[0].Address); 134 | Assert.Equal(1, person.Phones.Count); 135 | Assert.Equal("8011115555", person.Phones[0].Number); 136 | } 137 | finally 138 | { 139 | // Ensure the changes here don't affect other unit tests 140 | using (var db = fixture.GetDbConnection()) 141 | { 142 | if (emailId != default(int)) 143 | { 144 | SqlBuilder 145 | .Delete("PersonEmail", new { EmailId = emailId }) 146 | .Delete("Email", new { Id = emailId }) 147 | .Execute(db); 148 | } 149 | 150 | if (phoneId != default(int)) 151 | { 152 | SqlBuilder 153 | .Delete("PersonPhone", new { PhoneId = phoneId }) 154 | .Delete("Phone", new { Id = phoneId }) 155 | .Execute(db); 156 | } 157 | 158 | if (personId != default(int)) 159 | { 160 | SqlBuilder 161 | .Delete(new { Id = personId }) 162 | .Execute(db); 163 | } 164 | } 165 | } 166 | } 167 | 168 | [Fact(DisplayName = "INSERT person asynchronously succeeds")] 169 | public async Task InsertPersonAsync() 170 | { 171 | Person person = null; 172 | // Ensure inserting a person works and we get IDs back 173 | int emailId = -1; 174 | int personId = -1; 175 | int phoneId = -1; 176 | 177 | try 178 | { 179 | using (var db = fixture.GetDbConnection()) 180 | { 181 | db.Open(); 182 | 183 | // Get the next identity aggressively, as we need to assign 184 | // it to both Id/MergedToPersonId 185 | personId = PostgreSql.NextIdentity(db, (Person p) => p.Id); 186 | Assert.True(personId > 0); 187 | 188 | person = new Person 189 | { 190 | Id = personId, 191 | FirstName = "Steven", 192 | LastName = "Rollman", 193 | MergedToPersonId = personId, 194 | }; 195 | 196 | int insertedCount; 197 | insertedCount = await SqlBuilder 198 | .Insert(person) 199 | .ExecuteAsync(db); 200 | Assert.Equal(1, insertedCount); 201 | 202 | emailId = PostgreSql.NextIdentity(db, (Email e) => e.Id); 203 | var email = new Email 204 | { 205 | Id = emailId, 206 | Address = "srollman@landmarkhw.com", 207 | }; 208 | 209 | var personEmail = new 210 | { 211 | PersonId = personId, 212 | EmailId = emailId, 213 | }; 214 | 215 | phoneId = PostgreSql.NextIdentity(db, (Phone p) => p.Id); 216 | var phone = new Phone 217 | { 218 | Id = phoneId, 219 | Number = "8011115555", 220 | Type = PhoneType.Mobile, 221 | }; 222 | 223 | var personPhone = new 224 | { 225 | PersonId = personId, 226 | PhoneId = phoneId, 227 | }; 228 | 229 | // Add email and phone number to the person 230 | insertedCount = await SqlBuilder 231 | .Insert(email) 232 | .Insert(phone) 233 | .Insert("PersonEmail", personEmail) 234 | .Insert("PersonPhone", personPhone) 235 | .ExecuteAsync(db); 236 | 237 | // Ensure all were inserted properly 238 | Assert.Equal(4, insertedCount); 239 | 240 | // Build an entity mapper for person 241 | var personMapper = new PersonEntityMapper(); 242 | 243 | // Query the person from the database 244 | var query = SqlBuilder 245 | .From("person") 246 | .LeftJoin("PersonEmail personEmail on person.Id = personEmail.Id") 247 | .LeftJoin("Email email on personEmail.EmailId = email.Id") 248 | .LeftJoin("PersonPhone personPhone on person.Id = personPhone.PersonId") 249 | .LeftJoin("Phone phone on personPhone.PhoneId = phone.Id") 250 | .Select("person.*, email.*, phone.*") 251 | .SplitOn("Id") 252 | .SplitOn("Id") 253 | .SplitOn("Id") 254 | .Where("person.Id = @id", new { id = personId }); 255 | 256 | var graphql = @" 257 | { 258 | person { 259 | firstName 260 | lastName 261 | emails { 262 | id 263 | address 264 | } 265 | phones { 266 | id 267 | number 268 | } 269 | } 270 | }"; 271 | var selection = fixture.BuildGraphQLSelection(graphql); 272 | 273 | var people = await query.ExecuteAsync(db, selection, personMapper); 274 | person = people 275 | .FirstOrDefault(); 276 | } 277 | 278 | // Ensure all inserted data is present 279 | Assert.NotNull(person); 280 | Assert.Equal(personId, person.Id); 281 | Assert.Equal("Steven", person.FirstName); 282 | Assert.Equal("Rollman", person.LastName); 283 | Assert.Equal(1, person.Emails.Count); 284 | Assert.Equal("srollman@landmarkhw.com", person.Emails[0].Address); 285 | Assert.Equal(1, person.Phones.Count); 286 | Assert.Equal("8011115555", person.Phones[0].Number); 287 | } 288 | finally 289 | { 290 | // Ensure the changes here don't affect other unit tests 291 | using (var db = fixture.GetDbConnection()) 292 | { 293 | if (emailId != default(int)) 294 | { 295 | await SqlBuilder 296 | .Delete("PersonEmail", new { EmailId = emailId }) 297 | .Delete("Email", new { Id = emailId }) 298 | .ExecuteAsync(db); 299 | } 300 | 301 | if (phoneId != default(int)) 302 | { 303 | await SqlBuilder 304 | .Delete("PersonPhone", new { PhoneId = phoneId }) 305 | .Delete("Phone", new { Id = phoneId }) 306 | .ExecuteAsync(db); 307 | } 308 | 309 | if (personId != default(int)) 310 | { 311 | await SqlBuilder 312 | .Delete(new { Id = personId }) 313 | .ExecuteAsync(db); 314 | } 315 | } 316 | } 317 | } 318 | } 319 | } -------------------------------------------------------------------------------- /Dapper.GraphQL.Test/Models/Company.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Dapper.GraphQL.Test.Models 6 | { 7 | public class Company 8 | { 9 | public int Id { get; set; } 10 | public string Name { get; set; } 11 | public IList Emails { get; set; } 12 | public IList Phones { get; set; } 13 | 14 | public Company() 15 | { 16 | Emails = new List(); 17 | Phones = new List(); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Dapper.GraphQL.Test/Models/Email.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Dapper.GraphQL.Test.Models 6 | { 7 | public class Email 8 | { 9 | public string Address { get; set; } 10 | public int Id { get; set; } 11 | } 12 | } -------------------------------------------------------------------------------- /Dapper.GraphQL.Test/Models/Person.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Dapper.GraphQL.Test.Models 6 | { 7 | public class Person 8 | { 9 | public Person CareerCounselor { get; set; } 10 | public IList Companies { get; set; } 11 | public IList Emails { get; set; } 12 | public string FirstName { get; set; } 13 | public int Id { get; set; } 14 | public string LastName { get; set; } 15 | public int MergedToPersonId { get; set; } 16 | public IList Phones { get; set; } 17 | public Person Supervisor { get; set; } 18 | 19 | public Person() 20 | { 21 | Companies = new List(); 22 | Emails = new List(); 23 | Phones = new List(); 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /Dapper.GraphQL.Test/Models/Phone.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Dapper.GraphQL.Test.Models 6 | { 7 | public enum PhoneType 8 | { 9 | Unknown = 0, 10 | Home = 1, 11 | Work = 2, 12 | Mobile = 3, 13 | Other = 4 14 | } 15 | 16 | public class Phone 17 | { 18 | public int Id { get; set; } 19 | public string Number { get; set; } 20 | public PhoneType Type { get; set; } 21 | } 22 | } -------------------------------------------------------------------------------- /Dapper.GraphQL.Test/QueryBuilders/CompanyQueryBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using GraphQL.Language.AST; 5 | using Dapper.GraphQL.Test.Models; 6 | 7 | namespace Dapper.GraphQL.Test.QueryBuilders 8 | { 9 | public class CompanyQueryBuilder : 10 | IQueryBuilder 11 | { 12 | private readonly IQueryBuilder emailQueryBuilder; 13 | private readonly IQueryBuilder phoneQueryBuilder; 14 | 15 | public CompanyQueryBuilder( 16 | IQueryBuilder emailQueryBuilder, 17 | IQueryBuilder phoneQueryBuilder) 18 | { 19 | this.emailQueryBuilder = emailQueryBuilder; 20 | this.phoneQueryBuilder = phoneQueryBuilder; 21 | } 22 | 23 | public SqlQueryContext Build(SqlQueryContext query, IHaveSelectionSet context, string alias) 24 | { 25 | query.Select($"{alias}.Id"); 26 | query.SplitOn("Id"); 27 | 28 | var fields = context.GetSelectedFields(); 29 | 30 | if (fields.ContainsKey("name")) 31 | { 32 | query.Select($"{alias}.Name"); 33 | } 34 | if (fields.ContainsKey("emails")) 35 | { 36 | var companyEmailAlias = $"{alias}CompanyEmail"; 37 | var emailAlias = $"{alias}Email"; 38 | query 39 | .LeftJoin($"CompanyEmail {companyEmailAlias} ON {alias}.Id = {companyEmailAlias}.PersonId") 40 | .LeftJoin($"Email {emailAlias} ON {companyEmailAlias}.EmailId = {emailAlias}.Id"); 41 | query = emailQueryBuilder.Build(query, fields["emails"], emailAlias); 42 | } 43 | if (fields.ContainsKey("phones")) 44 | { 45 | var companyPhoneAlias = $"{alias}CompanyPhone"; 46 | var phoneAlias = $"{alias}Phone"; 47 | query 48 | .LeftJoin($"CompanyPhone {companyPhoneAlias} ON {alias}.Id = {companyPhoneAlias}.PersonId") 49 | .LeftJoin($"Phone {phoneAlias} ON {companyPhoneAlias}.PhoneId = {phoneAlias}.Id"); 50 | query = phoneQueryBuilder.Build(query, fields["phones"], phoneAlias); 51 | } 52 | 53 | return query; 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /Dapper.GraphQL.Test/QueryBuilders/EmailQueryBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using GraphQL.Language.AST; 5 | using Dapper.GraphQL.Test.Models; 6 | 7 | namespace Dapper.GraphQL.Test.QueryBuilders 8 | { 9 | public class EmailQueryBuilder : 10 | IQueryBuilder 11 | { 12 | public SqlQueryContext Build(SqlQueryContext query, IHaveSelectionSet context, string alias) 13 | { 14 | // Always get the ID of the email 15 | query.Select($"{alias}.Id"); 16 | // Tell Dapper where the Email class begins (at the Id we just selected) 17 | query.SplitOn("Id"); 18 | 19 | var fields = context.GetSelectedFields(); 20 | if (fields.ContainsKey("address")) 21 | { 22 | query.Select($"{alias}.Address"); 23 | } 24 | 25 | return query; 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /Dapper.GraphQL.Test/QueryBuilders/PersonQueryBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using GraphQL.Language.AST; 5 | using Dapper.GraphQL.Test.Models; 6 | 7 | namespace Dapper.GraphQL.Test.QueryBuilders 8 | { 9 | public class PersonQueryBuilder : 10 | IQueryBuilder 11 | { 12 | private readonly IQueryBuilder companyQueryBuilder; 13 | private readonly IQueryBuilder emailQueryBuilder; 14 | private readonly IQueryBuilder phoneQueryBuilder; 15 | 16 | public PersonQueryBuilder( 17 | IQueryBuilder companyQueryBuilder, 18 | IQueryBuilder emailQueryBuilder, 19 | IQueryBuilder phoneQueryBuilder) 20 | { 21 | this.companyQueryBuilder = companyQueryBuilder; 22 | this.emailQueryBuilder = emailQueryBuilder; 23 | this.phoneQueryBuilder = phoneQueryBuilder; 24 | } 25 | 26 | public SqlQueryContext Build(SqlQueryContext query, IHaveSelectionSet context, string alias) 27 | { 28 | var mergedAlias = $"{alias}Merged"; 29 | 30 | // Deduplicate the person 31 | query.LeftJoin($"Person {mergedAlias} ON {alias}.MergedToPersonId = {mergedAlias}.MergedToPersonId"); 32 | query.Select($"{alias}.Id", $"{alias}.MergedToPersonId"); 33 | query.SplitOn("Id"); 34 | 35 | var fields = context.GetSelectedFields(); 36 | 37 | if (fields.ContainsKey("firstName")) 38 | { 39 | query.Select($"{mergedAlias}.FirstName"); 40 | } 41 | if (fields.ContainsKey("lastName")) 42 | { 43 | query.Select($"{mergedAlias}.LastName"); 44 | } 45 | if (fields.ContainsKey("companies")) 46 | { 47 | var personCompanyAlias = $"{alias}PersonCompany"; 48 | var companyAlias = $"{alias}Company"; 49 | query 50 | .LeftJoin($"PersonCompany {personCompanyAlias} ON {mergedAlias}.Id = {personCompanyAlias}.PersonId") 51 | .LeftJoin($"Company {companyAlias} ON {personCompanyAlias}.CompanyId = {companyAlias}.Id"); 52 | query = companyQueryBuilder.Build(query, fields["companies"], companyAlias); 53 | } 54 | if (fields.ContainsKey("emails")) 55 | { 56 | var personEmailAlias = $"{alias}PersonEmail"; 57 | var emailAlias = $"{alias}Email"; 58 | query 59 | .LeftJoin($"PersonEmail {personEmailAlias} ON {mergedAlias}.Id = {personEmailAlias}.PersonId") 60 | .LeftJoin($"Email {emailAlias} ON {personEmailAlias}.EmailId = {emailAlias}.Id"); 61 | query = emailQueryBuilder.Build(query, fields["emails"], emailAlias); 62 | } 63 | if (fields.ContainsKey("phones")) 64 | { 65 | var personPhoneAlias = $"{alias}PersonPhone"; 66 | var phoneAlias = $"{alias}Phone"; 67 | query 68 | .LeftJoin($"PersonPhone {personPhoneAlias} ON {mergedAlias}.Id = {personPhoneAlias}.PersonId") 69 | .LeftJoin($"Phone {phoneAlias} ON {personPhoneAlias}.PhoneId = {phoneAlias}.Id"); 70 | query = phoneQueryBuilder.Build(query, fields["phones"], phoneAlias); 71 | } 72 | if (fields.ContainsKey("supervisor")) 73 | { 74 | var supervisorAlias = $"{alias}Supervisor"; 75 | query.LeftJoin($"Person {supervisorAlias} ON {mergedAlias}.SupervisorId = {supervisorAlias}.Id"); 76 | query = Build(query, fields["supervisor"], supervisorAlias); 77 | } 78 | if (fields.ContainsKey("careerCounselor")) 79 | { 80 | var careerCounselorAlias = $"{alias}CareerCounselor"; 81 | query.LeftJoin($"Person {careerCounselorAlias} ON {mergedAlias}.CareerCounselorId = {careerCounselorAlias}.Id"); 82 | query = Build(query, fields["careerCounselor"], careerCounselorAlias); 83 | } 84 | 85 | return query; 86 | } 87 | } 88 | } -------------------------------------------------------------------------------- /Dapper.GraphQL.Test/QueryBuilders/PhoneQueryBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using GraphQL.Language.AST; 5 | using Dapper.GraphQL.Test.Models; 6 | 7 | namespace Dapper.GraphQL.Test.QueryBuilders 8 | { 9 | public class PhoneQueryBuilder : 10 | IQueryBuilder 11 | { 12 | public SqlQueryContext Build(SqlQueryContext query, IHaveSelectionSet context, string alias) 13 | { 14 | query.Select($"{alias}.Id"); 15 | query.SplitOn("Id"); 16 | 17 | var fields = context.GetSelectedFields(); 18 | foreach (var kvp in fields) 19 | { 20 | switch (kvp.Key) 21 | { 22 | case "number": query.Select($"{alias}.Number"); break; 23 | case "type": query.Select($"{alias}.Type"); break; 24 | } 25 | } 26 | 27 | return query; 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /Dapper.GraphQL.Test/QueryTests.cs: -------------------------------------------------------------------------------- 1 | using Dapper.GraphQL.Test.EntityMappers; 2 | using Dapper.GraphQL.Test.Models; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using System.Data.SqlClient; 5 | using Xunit; 6 | 7 | namespace Dapper.GraphQL.Test 8 | { 9 | public class QueryTests : IClassFixture 10 | { 11 | private readonly TestFixture fixture; 12 | 13 | public QueryTests(TestFixture fixture) 14 | { 15 | this.fixture = fixture; 16 | } 17 | 18 | [Fact(DisplayName = "ORDER BY should work")] 19 | public void OrderByShouldWork() 20 | { 21 | var query = SqlBuilder 22 | .From("Person person") 23 | .Select("person.Id") 24 | .SplitOn("Id") 25 | .OrderBy("LastName"); 26 | 27 | Assert.Contains("ORDER BY", query.ToString()); 28 | } 29 | 30 | [Fact(DisplayName = "SELECT without matching alias should throw")] 31 | public void SelectWithoutMatchingAliasShouldThrow() 32 | { 33 | Assert.Throws(() => 34 | { 35 | var query = SqlBuilder 36 | .From("Person person") 37 | .Select("person.Id", "notAnAlias.Id") 38 | .SplitOn("Id"); 39 | 40 | var graphql = "{ person { id } }"; 41 | var selectionSet = fixture.BuildGraphQLSelection(graphql); 42 | 43 | using (var db = fixture.GetDbConnection()) 44 | { 45 | query.Execute(db, selectionSet); 46 | } 47 | }); 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /Dapper.GraphQL.Test/Sql/1-Create.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE Person ( 2 | Id INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, 3 | -- Used to identify the person that this has been merged with 4 | -- (for deduplicating person entities in the db) 5 | MergedToPersonId INTEGER, 6 | FirstName VARCHAR(50), 7 | LastName VARCHAR(50), 8 | -- Known issue with FK reference to non-null numeric types 9 | -- https://github.com/StackExchange/Dapper/issues/917 10 | SupervisorId INTEGER, 11 | CareerCounselorId INTEGER, 12 | FOREIGN KEY (MergedToPersonId) REFERENCES Person(Id), 13 | FOREIGN KEY (SupervisorId) REFERENCES Person(Id), 14 | FOREIGN KEY (CareerCounselorId) REFERENCES Person(Id) 15 | ); 16 | 17 | CREATE TABLE Company ( 18 | Id INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, 19 | Name VARCHAR(100) 20 | ); 21 | 22 | CREATE TABLE PersonCompany ( 23 | Id INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, 24 | PersonId INTEGER NOT NULL, 25 | CompanyId INTEGER NOT NULL, 26 | StartDate DATE NOT NULL, 27 | EndDate DATE, 28 | FOREIGN KEY(PersonId) REFERENCES Person(Id), 29 | FOREIGN KEY(CompanyId) REFERENCES Company(Id) 30 | ); 31 | 32 | CREATE TABLE Email ( 33 | Id INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, 34 | Address VARCHAR(250) 35 | ); 36 | 37 | CREATE TABLE PersonEmail ( 38 | Id INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, 39 | PersonId INTEGER NOT NULL, 40 | EmailId INTEGER NOT NULL, 41 | FOREIGN KEY(PersonId) REFERENCES Person(Id), 42 | FOREIGN KEY(EmailId) REFERENCES Email(Id) 43 | ); 44 | 45 | CREATE TABLE CompanyEmail ( 46 | Id INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, 47 | CompanyId INTEGER NOT NULL, 48 | EmailId INTEGER NOT NULL, 49 | FOREIGN KEY(CompanyId) REFERENCES Company(Id), 50 | FOREIGN KEY(EmailId) REFERENCES Email(Id) 51 | ); 52 | 53 | CREATE TABLE Phone ( 54 | Id INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, 55 | Number VARCHAR(16), 56 | Type INTEGER 57 | ); 58 | 59 | CREATE TABLE PersonPhone ( 60 | Id INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, 61 | PersonId INTEGER NOT NULL, 62 | PhoneId INTEGER NOT NULL, 63 | FOREIGN KEY(PersonId) REFERENCES Person(Id), 64 | FOREIGN KEY(PhoneId) REFERENCES Phone(Id) 65 | ); 66 | 67 | CREATE TABLE CompanyPhone ( 68 | Id INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, 69 | CompanyId INTEGER NOT NULL, 70 | PhoneId INTEGER NOT NULL, 71 | FOREIGN KEY(CompanyId) REFERENCES Company(Id), 72 | FOREIGN KEY(PhoneId) REFERENCES Phone(Id) 73 | ); -------------------------------------------------------------------------------- /Dapper.GraphQL.Test/Sql/2-Data.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO Person (Id, MergedToPersonId, FirstName, LastName, SupervisorId, CareerCounselorId) VALUES (1, 1, 'Hyrum', 'Clyde', NULL, NULL); 2 | INSERT INTO Person (Id, MergedToPersonId, FirstName, LastName, SupervisorId, CareerCounselorId) VALUES (2, 2, 'Doug', 'Day', NULL, NULL); 3 | INSERT INTO Person (Id, MergedToPersonId, FirstName, LastName, SupervisorId, CareerCounselorId) VALUES (3, 3, 'Kevin', 'Russon', 1, 2); 4 | INSERT INTO Person (Id, MergedToPersonId, FirstName, LastName, SupervisorId, CareerCounselorId) VALUES (4, 4, 'Douglas', 'Day', NULL, 1); 5 | -- Update the identity value 6 | SELECT setval(pg_get_serial_sequence('person', 'id'), (SELECT MAX(Id) FROM Person)); 7 | 8 | -- Merge people (Doug == Douglas) 9 | UPDATE Person 10 | SET MergedToPersonId = 4 11 | WHERE Id = 2; 12 | 13 | INSERT INTO Company (Id, Name) VALUES (1, 'Landmark Home Warranty, LLC'); 14 | INSERT INTO Company (Id, Name) VALUES (2, 'Navitaire, LLC'); 15 | SELECT setval(pg_get_serial_sequence('company', 'id'), (SELECT MAX(Id) FROM Company)); 16 | 17 | INSERT INTO PersonCompany (Id, PersonId, CompanyId, StartDate, EndDate) VALUES (1, 1, 1, '2016-01-01', NULL); 18 | INSERT INTO PersonCompany (Id, PersonId, CompanyId, StartDate, EndDate) VALUES (2, 2, 1, '2016-05-16', NULL); 19 | INSERT INTO PersonCompany (Id, PersonId, CompanyId, StartDate, EndDate) VALUES (3, 3, 1, '2016-10-05', NULL); 20 | INSERT INTO PersonCompany (Id, PersonId, CompanyId, StartDate, EndDate) VALUES (4, 4, 2, '2011-04-06', '2016-05-13'); 21 | INSERT INTO PersonCompany (Id, PersonId, CompanyId, StartDate, EndDate) VALUES (5, 3, 2, '2011-08-15', '2016-10-02'); 22 | SELECT setval(pg_get_serial_sequence('personcompany', 'id'), (SELECT MAX(Id) FROM PersonCompany)); 23 | 24 | INSERT INTO Email (Id, Address) VALUES (1, 'hclyde@landmarkhw.com'); 25 | INSERT INTO Email (Id, Address) VALUES (2, 'dday@landmarkhw.com'); 26 | INSERT INTO Email (Id, Address) VALUES (3, 'dougrday@gmail.com'); 27 | INSERT INTO Email (Id, Address) VALUES (4, 'krusson@landmarkhw.com'); 28 | INSERT INTO Email (Id, Address) VALUES (5, 'whole-company@landmarkhw.com'); 29 | INSERT INTO Email (Id, Address) VALUES (6, 'whole-company@navitaire.com'); 30 | SELECT setval(pg_get_serial_sequence('email', 'id'), (SELECT MAX(Id) FROM Email)); 31 | 32 | INSERT INTO PersonEmail (Id, EmailId, PersonId) VALUES (1, 1, 1); 33 | INSERT INTO PersonEmail (Id, EmailId, PersonId) VALUES (2, 2, 2); 34 | INSERT INTO PersonEmail (Id, EmailId, PersonId) VALUES (3, 3, 4); 35 | INSERT INTO PersonEmail (Id, EmailId, PersonId) VALUES (4, 4, 3); 36 | SELECT setval(pg_get_serial_sequence('personemail', 'id'), (SELECT MAX(Id) FROM PersonEmail)); 37 | 38 | INSERT INTO CompanyEmail (Id, CompanyId, EmailId) VALUES (1, 1, 5); 39 | INSERT INTO CompanyEmail (Id, CompanyId, EmailId) VALUES (2, 2, 6); 40 | SELECT setval(pg_get_serial_sequence('companyemail', 'id'), (SELECT MAX(Id) FROM CompanyEmail)); 41 | 42 | INSERT INTO Phone (Id, Number, Type) VALUES (1, '8011234567', 3); 43 | INSERT INTO Phone (Id, Number, Type) VALUES (2, '8019876543', 3); 44 | INSERT INTO Phone (Id, Number, Type) VALUES (3, '8011111111', 1); 45 | INSERT INTO Phone (Id, Number, Type) VALUES (4, '8663062999', 1); 46 | INSERT INTO Phone (Id, Number, Type) VALUES (5, '8019477800', 1); 47 | SELECT setval(pg_get_serial_sequence('phone', 'id'), (SELECT MAX(Id) FROM Phone)); 48 | 49 | INSERT INTO PersonPhone (Id, PhoneId, PersonId) VALUES (1, 1, 2); 50 | INSERT INTO PersonPhone (Id, PhoneId, PersonId) VALUES (2, 2, 3); 51 | INSERT INTO PersonPhone (Id, PhoneId, PersonId) VALUES (3, 3, 3); 52 | SELECT setval(pg_get_serial_sequence('personphone', 'id'), (SELECT MAX(Id) FROM PersonPhone)); 53 | 54 | INSERT INTO CompanyPhone (Id, PhoneId, CompanyId) VALUES (1, 4, 1); 55 | INSERT INTO CompanyPhone (Id, PhoneId, CompanyId) VALUES (2, 5, 2); 56 | SELECT setval(pg_get_serial_sequence('companyphone', 'id'), (SELECT MAX(Id) FROM CompanyPhone)); 57 | -------------------------------------------------------------------------------- /Dapper.GraphQL.Test/TestFixture.cs: -------------------------------------------------------------------------------- 1 | using Dapper.GraphQL.Test.GraphQL; 2 | using Dapper.GraphQL.Test.Models; 3 | using Dapper.GraphQL.Test.QueryBuilders; 4 | using DbUp; 5 | using GraphQL; 6 | using GraphQL.Execution; 7 | using GraphQL.Http; 8 | using GraphQL.Language.AST; 9 | using Microsoft.Extensions.DependencyInjection; 10 | using Newtonsoft.Json.Linq; 11 | using Npgsql; 12 | using System; 13 | using System.Collections.Generic; 14 | using System.Data; 15 | using System.Linq; 16 | using System.Reflection; 17 | using System.Threading.Tasks; 18 | 19 | namespace Dapper.GraphQL.Test 20 | { 21 | public class TestFixture : IDisposable 22 | { 23 | #region Statics 24 | 25 | private static string chars = "abcdefghijklmnopqrstuvwxyz0123456789"; 26 | private static Random random = new Random((int)(DateTime.Now.Ticks << 32)); 27 | 28 | #endregion Statics 29 | 30 | private readonly string DatabaseName; 31 | private readonly DocumentExecuter DocumentExecuter; 32 | public PersonSchema Schema { get; set; } 33 | public IServiceProvider ServiceProvider { get; set; } 34 | private string ConnectionString { get; set; } = null; 35 | private bool IsDisposing { get; set; } = false; 36 | 37 | public TestFixture() 38 | { 39 | DatabaseName = "test-" + new string(chars.OrderBy(c => random.Next()).ToArray()); 40 | 41 | DocumentExecuter = new DocumentExecuter(); 42 | var serviceCollection = new ServiceCollection(); 43 | 44 | SetupDatabaseConnection(); 45 | SetupDapperGraphQL(serviceCollection); 46 | 47 | ServiceProvider = serviceCollection.BuildServiceProvider(); 48 | Schema = ServiceProvider.GetRequiredService(); 49 | } 50 | 51 | public IHaveSelectionSet BuildGraphQLSelection(string body) 52 | { 53 | var document = new GraphQLDocumentBuilder().Build(body); 54 | return document 55 | .Operations 56 | .OfType() 57 | .First()? 58 | .SelectionSet 59 | .Selections 60 | .OfType() 61 | .FirstOrDefault(); 62 | } 63 | 64 | public void Dispose() 65 | { 66 | if (!IsDisposing) 67 | { 68 | IsDisposing = true; 69 | TeardownDatabase(); 70 | } 71 | } 72 | 73 | public IDbConnection GetDbConnection() 74 | { 75 | var connection = new NpgsqlConnection(ConnectionString); 76 | return connection; 77 | } 78 | 79 | public bool JsonEquals(string expectedJson, string actualJson) 80 | { 81 | // To ensure formatting doesn't affect our results, we first convert to JSON tokens 82 | // and only compare the structure of the resulting objects. 83 | return JToken.DeepEquals(JObject.Parse(expectedJson), JObject.Parse(actualJson)); 84 | } 85 | 86 | public async Task QueryGraphQLAsync(string query) 87 | { 88 | var result = await DocumentExecuter 89 | .ExecuteAsync(options => 90 | { 91 | options.Schema = Schema; 92 | options.Query = query; 93 | }) 94 | .ConfigureAwait(false); 95 | 96 | var json = new DocumentWriter(indent: true).Write(result); 97 | return json; 98 | } 99 | 100 | public async Task QueryGraphQLAsync(GraphQlQuery query) 101 | { 102 | var result = await DocumentExecuter 103 | .ExecuteAsync(options => 104 | { 105 | options.Schema = Schema; 106 | options.Query = query.Query; 107 | options.Inputs = query.Variables != null ? new Inputs(StringExtensions.GetValue(query.Variables) as Dictionary) : null; 108 | }) 109 | .ConfigureAwait(false); 110 | 111 | var json = new DocumentWriter(indent: true).Write(result); 112 | return json; 113 | } 114 | 115 | public void SetupDatabaseConnection() 116 | { 117 | // Generate a random db name 118 | 119 | ConnectionString = $"Server=localhost;Port=5432;Database={DatabaseName};User Id=postgres;Password=dapper-graphql;"; 120 | 121 | // Ensure the database exists 122 | EnsureDatabase.For.PostgresqlDatabase(ConnectionString); 123 | 124 | var upgrader = DeployChanges.To 125 | .PostgresqlDatabase(ConnectionString) 126 | .WithScriptsEmbeddedInAssembly(typeof(Person).GetTypeInfo().Assembly) 127 | .LogToConsole() 128 | .Build(); 129 | 130 | var upgradeResult = upgrader.PerformUpgrade(); 131 | if (!upgradeResult.Successful) 132 | { 133 | throw new InvalidOperationException("The database upgrade did not succeed for unit testing.", upgradeResult.Error); 134 | } 135 | } 136 | 137 | public void TeardownDatabase() 138 | { 139 | // Connect to a different database, so we can drop the one we were working with 140 | var dropConnectionString = ConnectionString.Replace(DatabaseName, "template1"); 141 | using (var connection = new NpgsqlConnection(dropConnectionString)) 142 | { 143 | connection.Open(); 144 | var command = connection.CreateCommand(); 145 | 146 | // NOTE: I'm not sure why there are active connections to the database at 147 | // this point, as we're the only ones using this database, and the connection 148 | // is closed at this point. In any case, we need to take an extra step of 149 | // dropping all connections to the database before dropping it. 150 | // 151 | // See http://www.leeladharan.com/drop-a-postgresql-database-if-there-are-active-connections-to-it 152 | command.CommandText = $@" 153 | SELECT pg_terminate_backend(pg_stat_activity.pid) 154 | FROM pg_stat_activity 155 | WHERE pg_stat_activity.datname = '{DatabaseName}' AND pid <> pg_backend_pid(); 156 | 157 | DROP DATABASE ""{DatabaseName}"";"; 158 | command.CommandType = CommandType.Text; 159 | 160 | // Drop the database 161 | command.ExecuteNonQuery(); 162 | } 163 | } 164 | 165 | private void SetupDapperGraphQL(IServiceCollection serviceCollection) 166 | { 167 | serviceCollection.AddSingleton(s => new FuncDependencyResolver(s.GetRequiredService)); 168 | 169 | serviceCollection.AddDapperGraphQL(options => 170 | { 171 | // Add GraphQL types 172 | options.AddType(); 173 | options.AddType(); 174 | options.AddType(); 175 | options.AddType(); 176 | options.AddType(); 177 | options.AddType(); 178 | options.AddType(); 179 | 180 | // Add the GraphQL schema 181 | options.AddSchema(); 182 | 183 | // Add query builders for dapper 184 | options.AddQueryBuilder(); 185 | options.AddQueryBuilder(); 186 | options.AddQueryBuilder(); 187 | options.AddQueryBuilder(); 188 | }); 189 | 190 | serviceCollection.AddTransient(serviceProvider => GetDbConnection()); 191 | } 192 | } 193 | } -------------------------------------------------------------------------------- /Dapper.GraphQL.Test/UpdateTests.cs: -------------------------------------------------------------------------------- 1 | using Dapper.GraphQL.Test.EntityMappers; 2 | using Dapper.GraphQL.Test.Models; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Xunit; 7 | 8 | namespace Dapper.GraphQL.Test 9 | { 10 | public class UpdateTests : IClassFixture 11 | { 12 | private readonly TestFixture fixture; 13 | 14 | public UpdateTests(TestFixture fixture) 15 | { 16 | this.fixture = fixture; 17 | } 18 | 19 | [Fact(DisplayName = "UPDATE person succeeds")] 20 | public void UpdatePerson() 21 | { 22 | Person person = new Person 23 | { 24 | FirstName = "Douglas" 25 | }; 26 | Person previousPerson = null; 27 | 28 | try 29 | { 30 | var graphql = @" 31 | { 32 | person { 33 | id 34 | firstName 35 | } 36 | }"; 37 | 38 | var selectionSet = fixture.BuildGraphQLSelection(graphql); 39 | 40 | // Update the person with Id = 2 with a new FirstName 41 | using (var db = fixture.GetDbConnection()) 42 | { 43 | previousPerson = SqlBuilder 44 | .From() 45 | .Select("Id", "FirstName") 46 | .Where("FirstName = @firstName", new { firstName = "Doug" }) 47 | .Execute(db, selectionSet) 48 | .FirstOrDefault(); 49 | 50 | SqlBuilder 51 | .Update(person) 52 | .Where("Id = @id", new { id = previousPerson.Id }) 53 | .Execute(db); 54 | 55 | // Get the same person back 56 | person = SqlBuilder 57 | .From() 58 | .Select("Id", "FirstName") 59 | .Where("Id = @id", new { id = previousPerson.Id }) 60 | .Execute(db, selectionSet) 61 | .FirstOrDefault(); 62 | } 63 | 64 | // Ensure we got a person and their name was indeed changed 65 | Assert.NotNull(person); 66 | Assert.Equal("Douglas", person.FirstName); 67 | } 68 | finally 69 | { 70 | if (previousPerson != null) 71 | { 72 | using (var db = fixture.GetDbConnection()) 73 | { 74 | person = new Person 75 | { 76 | FirstName = previousPerson.FirstName 77 | }; 78 | 79 | // Put the entity back to the way it was 80 | SqlBuilder 81 | .Update(person) 82 | .Where("Id = @id", new { id = 2 }) 83 | .Execute(db); 84 | } 85 | } 86 | } 87 | } 88 | 89 | [Fact(DisplayName = "UPDATE person asynchronously succeeds")] 90 | public async Task UpdatePersonAsync() 91 | { 92 | Person person = new Person 93 | { 94 | FirstName = "Douglas" 95 | }; 96 | Person previousPerson = null; 97 | 98 | try 99 | { 100 | // Update the person with Id = 2 with a new FirstName 101 | using (var db = fixture.GetDbConnection()) 102 | { 103 | db.Open(); 104 | 105 | var graphql = @" 106 | { 107 | person { 108 | id 109 | firstName 110 | } 111 | }"; 112 | 113 | var selectionSet = fixture.BuildGraphQLSelection(graphql); 114 | 115 | var previousPeople = await SqlBuilder 116 | .From() 117 | .Select("Id", "FirstName") 118 | .Where("FirstName = @firstName", new { firstName = "Doug" }) 119 | .ExecuteAsync(db, selectionSet); 120 | 121 | previousPerson = previousPeople.FirstOrDefault(); 122 | 123 | await SqlBuilder 124 | .Update(person) 125 | .Where("Id = @id", new { id = previousPerson.Id }) 126 | .ExecuteAsync(db); 127 | 128 | // Get the same person back 129 | var people = await SqlBuilder 130 | .From() 131 | .Select("Id", "FirstName") 132 | .Where("Id = @id", new { id = previousPerson.Id }) 133 | .ExecuteAsync(db, selectionSet); 134 | person = people 135 | .FirstOrDefault(); 136 | } 137 | 138 | // Ensure we got a person and their name was indeed changed 139 | Assert.NotNull(person); 140 | Assert.Equal("Douglas", person.FirstName); 141 | } 142 | finally 143 | { 144 | if (previousPerson != null) 145 | { 146 | using (var db = fixture.GetDbConnection()) 147 | { 148 | db.Open(); 149 | 150 | person = new Person 151 | { 152 | FirstName = previousPerson.FirstName 153 | }; 154 | 155 | // Put the entity back to the way it was 156 | await SqlBuilder 157 | .Update(person) 158 | .Where("Id = @id", new { id = 2 }) 159 | .ExecuteAsync(db); 160 | } 161 | } 162 | } 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /Dapper.GraphQL.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26730.12 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapper.GraphQL", "Dapper.GraphQL\Dapper.GraphQL.csproj", "{9FE3BA2C-C50C-418F-ACAD-2433E693C688}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapper.GraphQL.Test", "Dapper.GraphQL.Test\Dapper.GraphQL.Test.csproj", "{C95C441C-B612-4D8B-9B54-734FDF1DEAA2}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {9FE3BA2C-C50C-418F-ACAD-2433E693C688}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {9FE3BA2C-C50C-418F-ACAD-2433E693C688}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {9FE3BA2C-C50C-418F-ACAD-2433E693C688}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {9FE3BA2C-C50C-418F-ACAD-2433E693C688}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {C95C441C-B612-4D8B-9B54-734FDF1DEAA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {C95C441C-B612-4D8B-9B54-734FDF1DEAA2}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {C95C441C-B612-4D8B-9B54-734FDF1DEAA2}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {C95C441C-B612-4D8B-9B54-734FDF1DEAA2}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {7D61D89B-1DF4-4793-9435-FBE49C326560} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /Dapper.GraphQL/Contexts/EntityMapContext.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Language.AST; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | namespace Dapper.GraphQL 7 | { 8 | public class EntityMapContext : IDisposable 9 | { 10 | private bool IsDisposing = false; 11 | private object LockObject = new object(); 12 | 13 | /// 14 | /// A list of objects to be mapped. 15 | /// 16 | public IEnumerable Items { get; set; } 17 | 18 | /// 19 | /// The count of objects that have been mapped. 20 | /// 21 | public int MappedCount { get; protected set; } = 0; 22 | 23 | /// 24 | /// The GraphQL selection criteria. 25 | /// 26 | public IHaveSelectionSet SelectionSet { get; set; } 27 | 28 | /// 29 | /// The types used to split the GraphQL query. 30 | /// 31 | public IEnumerable SplitOn { get; set; } 32 | 33 | protected IDictionary CurrentSelectionSet { get; set; } 34 | protected IEnumerator ItemEnumerator { get; set; } 35 | protected IEnumerator SplitOnEnumerator { get; set; } 36 | 37 | public void Dispose() 38 | { 39 | lock (LockObject) 40 | { 41 | if (!IsDisposing) 42 | { 43 | IsDisposing = true; 44 | 45 | if (ItemEnumerator != null && 46 | SplitOnEnumerator != null) 47 | { 48 | ItemEnumerator.Dispose(); 49 | ItemEnumerator = null; 50 | SplitOnEnumerator.Dispose(); 51 | SplitOnEnumerator = null; 52 | } 53 | } 54 | } 55 | } 56 | 57 | /// 58 | /// Returns a map of selected GraphQL fields. 59 | /// 60 | public IDictionary GetSelectedFields() 61 | { 62 | return SelectionSet.GetSelectedFields(); 63 | } 64 | 65 | /// 66 | /// Maps the next object from Dapper. 67 | /// 68 | /// The item type to be mapped. 69 | /// The context used to map object from Dapper. 70 | /// The names of one or more GraphQL fields associated with the item. 71 | /// An optional entity mapper. This is used to map complex objects from Dapper mapping results. 72 | /// The mapped item. 73 | public TItemType Next( 74 | IEnumerable fieldNames, 75 | Func, IHaveSelectionSet, IHaveSelectionSet> getSelectionSet, 76 | IEntityMapper entityMapper = null) 77 | where TItemType : class 78 | { 79 | if (fieldNames == null) 80 | { 81 | throw new ArgumentNullException(nameof(fieldNames)); 82 | } 83 | 84 | if (ItemEnumerator == null || 85 | SplitOnEnumerator == null) 86 | { 87 | throw new NotSupportedException("Cannot call Next() before calling Start()"); 88 | } 89 | 90 | lock (LockObject) 91 | { 92 | var keys = fieldNames.Intersect(CurrentSelectionSet.Keys); 93 | if (keys.Any()) 94 | { 95 | TItemType item = default(TItemType); 96 | while ( 97 | ItemEnumerator.MoveNext() && 98 | SplitOnEnumerator.MoveNext()) 99 | { 100 | // Whether a non-null object exists at this position or not, 101 | // the SplitOn is expecting this type here, so we will yield it. 102 | if (SplitOnEnumerator.Current == typeof(TItemType)) 103 | { 104 | item = ItemEnumerator.Current as TItemType; 105 | break; 106 | } 107 | } 108 | 109 | if (entityMapper != null) 110 | { 111 | // Determine where the next entity mapper will get its selection set from 112 | IHaveSelectionSet selectionSet = getSelectionSet(CurrentSelectionSet, SelectionSet); 113 | 114 | var nextContext = new EntityMapContext 115 | { 116 | Items = Items.Skip(MappedCount), 117 | SelectionSet = selectionSet, 118 | SplitOn = SplitOn.Skip(MappedCount), 119 | }; 120 | using (nextContext) 121 | { 122 | item = entityMapper.Map(nextContext); 123 | 124 | // Update enumerators to skip past items already mapped 125 | var mappedCount = nextContext.MappedCount; 126 | MappedCount += nextContext.MappedCount; 127 | int i = 0; 128 | while ( 129 | // Less 1, the next time we iterate we 130 | // will advance by 1 as part of the iteration. 131 | i < mappedCount - 1 && 132 | ItemEnumerator.MoveNext() && 133 | SplitOnEnumerator.MoveNext()) 134 | { 135 | i++; 136 | } 137 | } 138 | } 139 | else 140 | { 141 | MappedCount++; 142 | } 143 | return item; 144 | } 145 | } 146 | return default(TItemType); 147 | } 148 | 149 | /// 150 | /// Begins mapping objects from Dapper. 151 | /// 152 | /// The entity type to be mapped. 153 | /// The mapped entity. 154 | public TEntityType Start() 155 | where TEntityType : class 156 | { 157 | lock (LockObject) 158 | { 159 | ItemEnumerator = Items.GetEnumerator(); 160 | SplitOnEnumerator = SplitOn.GetEnumerator(); 161 | CurrentSelectionSet = SelectionSet.GetSelectedFields(); 162 | MappedCount = 0; 163 | 164 | if (ItemEnumerator.MoveNext() && 165 | SplitOnEnumerator.MoveNext()) 166 | { 167 | var entity = ItemEnumerator.Current as TEntityType; 168 | MappedCount++; 169 | return entity; 170 | } 171 | return default(TEntityType); 172 | } 173 | } 174 | } 175 | } -------------------------------------------------------------------------------- /Dapper.GraphQL/Contexts/SqlDeleteContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Data; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace Dapper.GraphQL 9 | { 10 | public class SqlDeleteContext 11 | { 12 | public DynamicParameters Parameters { get; set; } 13 | public string Table { get; private set; } 14 | private List Deletes { get; set; } 15 | 16 | public SqlDeleteContext( 17 | string table, 18 | dynamic parameters = null) 19 | { 20 | if (parameters != null && !(parameters is IEnumerable>)) 21 | { 22 | parameters = ParameterHelper.GetSetFlatProperties(parameters); 23 | } 24 | this.Parameters = new DynamicParameters(parameters); 25 | this.Table = table; 26 | } 27 | 28 | public static SqlDeleteContext Delete(dynamic parameters = null) 29 | { 30 | return new SqlDeleteContext(typeof(TEntityType).Name, parameters); 31 | } 32 | 33 | /// 34 | /// Adds an additional DELETE statement after this one. 35 | /// 36 | /// The table to delete data from. 37 | /// The data to be deleted. 38 | /// The context of the DELETE statement. 39 | public SqlDeleteContext Delete(string table, dynamic parameters = null) 40 | { 41 | if (Deletes == null) 42 | { 43 | Deletes = new List(); 44 | } 45 | var delete = SqlBuilder.Delete(table, parameters); 46 | Deletes.Add(delete); 47 | return this; 48 | } 49 | 50 | /// 51 | /// Executes the DELETE statement with Dapper, using the provided database connection. 52 | /// 53 | /// The database connection. 54 | /// The transaction to execute under (optional). 55 | /// The options for the command (optional). 56 | public int Execute(IDbConnection connection, IDbTransaction transaction = null, SqlMapperOptions options = null) 57 | { 58 | if (options == null) { 59 | options = SqlMapperOptions.DefaultOptions; 60 | } 61 | 62 | int result = connection.Execute(BuildSql(), Parameters, transaction, options.CommandTimeout, options.CommandType); 63 | if (Deletes != null) 64 | { 65 | // Execute each delete and aggregate the results 66 | result = Deletes.Aggregate(result, (current, delete) => current + delete.Execute(connection, transaction, options)); 67 | } 68 | return result; 69 | } 70 | 71 | /// 72 | /// Executes the DELETE statement with Dapper asynchronously, using the provided database connection. 73 | /// 74 | /// The database connection. 75 | /// The transaction to execute under (optional). 76 | /// The options for the command (optional). 77 | public async Task ExecuteAsync(IDbConnection connection, IDbTransaction transaction = null, SqlMapperOptions options = null) 78 | { 79 | if (options == null) { 80 | options = SqlMapperOptions.DefaultOptions; 81 | } 82 | 83 | int result = await connection.ExecuteAsync(BuildSql(), Parameters, transaction, options.CommandTimeout, options.CommandType); 84 | if (Deletes != null) 85 | { 86 | // Execute each delete and aggregate the results 87 | result = await Deletes.AggregateAsync(result, async (current, delete) => current + await delete.ExecuteAsync(connection, transaction, options)); 88 | } 89 | return result; 90 | } 91 | 92 | /// 93 | /// Renders the generated SQL statement. 94 | /// 95 | /// The rendered SQL statement. 96 | public override string ToString() 97 | { 98 | return BuildSql(); 99 | } 100 | 101 | /// 102 | /// Builds the DELETE statement. 103 | /// 104 | /// A SQL DELETE statement. 105 | private string BuildSql() 106 | { 107 | var sb = new StringBuilder(); 108 | sb.Append($"DELETE FROM {Table} WHERE "); 109 | sb.Append(string.Join(" AND ", Parameters.ParameterNames.Select(name => $"{name} = @{name}"))); 110 | return sb.ToString(); 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /Dapper.GraphQL/Contexts/SqlInsertContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Data; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace Dapper.GraphQL 9 | { 10 | public class SqlInsertContext : 11 | SqlInsertContext 12 | where TEntityType : class 13 | { 14 | private List> Inserts { get; set; } 15 | 16 | public SqlInsertContext(string table, TEntityType obj) 17 | : base(table, obj) 18 | { 19 | } 20 | 21 | /// 22 | /// Adds an additional INSERT statement after this one. 23 | /// 24 | /// The data to be inserted. 25 | /// The context of the INSERT statement. 26 | public virtual SqlInsertContext Insert(TEntityType obj) 27 | { 28 | if (Inserts == null) 29 | { 30 | Inserts = new List>(); 31 | } 32 | var insert = SqlBuilder.Insert(obj); 33 | Inserts.Add(insert); 34 | return this; 35 | } 36 | } 37 | 38 | public class SqlInsertContext 39 | { 40 | private HashSet InsertParameterNames; 41 | public DynamicParameters Parameters { get; set; } 42 | public string Table { get; private set; } 43 | private List Inserts { get; set; } 44 | 45 | public SqlInsertContext( 46 | string table, 47 | dynamic parameters = null) 48 | { 49 | if (parameters != null && !(parameters is IEnumerable>)) 50 | { 51 | parameters = ParameterHelper.GetSetFlatProperties(parameters); 52 | } 53 | this.Parameters = new DynamicParameters(parameters); 54 | this.InsertParameterNames = new HashSet(Parameters.ParameterNames); 55 | this.Table = table; 56 | } 57 | 58 | /// 59 | /// Executes the INSERT statements with Dapper, using the provided database connection. 60 | /// 61 | /// The database connection. 62 | /// The transaction to execute under (optional). 63 | /// The options for the command (optional). 64 | public int Execute(IDbConnection connection, IDbTransaction transaction = null, SqlMapperOptions options = null) 65 | { 66 | if (options == null) { 67 | options = SqlMapperOptions.DefaultOptions; 68 | } 69 | 70 | int result = connection.Execute(BuildSql(), Parameters, transaction, options.CommandTimeout, options.CommandType); 71 | if (Inserts != null) 72 | { 73 | // Execute each insert and aggregate the results 74 | result = Inserts.Aggregate(result, (current, insert) => current + insert.Execute(connection, transaction, options)); 75 | } 76 | return result; 77 | } 78 | 79 | /// 80 | /// Executes the INSERT statements with Dapper asynchronously, using the provided database connection. 81 | /// 82 | /// The database connection. 83 | /// The transaction to execute under (optional). 84 | /// The options for the command (optional). 85 | public async Task ExecuteAsync(IDbConnection connection, IDbTransaction transaction = null, SqlMapperOptions options = null) 86 | { 87 | if (options == null) { 88 | options = SqlMapperOptions.DefaultOptions; 89 | } 90 | 91 | int result = await connection.ExecuteAsync(BuildSql(), Parameters, transaction, options.CommandTimeout, options.CommandType); 92 | if (Inserts != null) 93 | { 94 | // Execute each insert and aggregate the results 95 | result = await Inserts.AggregateAsync(result, async (current, insert) => current + await insert.ExecuteAsync(connection, transaction, options)); 96 | } 97 | return result; 98 | } 99 | 100 | /// 101 | /// Adds an additional INSERT statement after this one. 102 | /// 103 | /// The type of entity to be inserted. 104 | /// The data to be inserted. 105 | /// The context of the INSERT statement. 106 | public virtual SqlInsertContext Insert(TEntityType obj) 107 | where TEntityType : class 108 | { 109 | if (Inserts == null) 110 | { 111 | Inserts = new List(); 112 | } 113 | var insert = SqlBuilder.Insert(obj); 114 | Inserts.Add(insert); 115 | return this; 116 | } 117 | 118 | /// 119 | /// Adds an additional INSERT statement after this one. 120 | /// 121 | /// The table to insert data into. 122 | /// The data to be inserted. 123 | /// The context of the INSERT statement. 124 | public SqlInsertContext Insert(string table, dynamic parameters = null) 125 | { 126 | if (Inserts == null) 127 | { 128 | Inserts = new List(); 129 | } 130 | var insert = SqlBuilder.Insert(table, parameters); 131 | Inserts.Add(insert); 132 | return this; 133 | } 134 | 135 | /// 136 | /// Renders the generated SQL statement. 137 | /// 138 | /// The rendered SQL statement. 139 | public override string ToString() 140 | { 141 | return BuildSql(); 142 | } 143 | 144 | /// 145 | /// Builds the INSERT statement. 146 | /// 147 | /// A SQL INSERT statement. 148 | private string BuildSql() 149 | { 150 | var sb = new StringBuilder(); 151 | sb.Append($"INSERT INTO {Table} ("); 152 | sb.Append(string.Join(", ", InsertParameterNames)); 153 | sb.Append(") VALUES ("); 154 | sb.Append(string.Join(", ", InsertParameterNames.Select(name => $"@{name}"))); 155 | sb.Append(");"); 156 | return sb.ToString(); 157 | } 158 | } 159 | } -------------------------------------------------------------------------------- /Dapper.GraphQL/Contexts/SqlQueryContext.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Language.AST; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Data; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | 8 | namespace Dapper.GraphQL 9 | { 10 | public class SqlQueryContext : 11 | SqlQueryContext 12 | where TEntityType : class 13 | { 14 | public SqlQueryContext(string alias = null, dynamic parameters = null) 15 | : base(alias == null ? typeof(TEntityType).Name : $"{typeof(TEntityType).Name} {alias}") 16 | { 17 | _types.Add(typeof(TEntityType)); 18 | } 19 | } 20 | 21 | public class SqlQueryContext 22 | { 23 | protected List _splitOn; 24 | protected List _types; 25 | 26 | public DynamicParameters Parameters { get; set; } 27 | protected Dapper.SqlBuilder SqlBuilder { get; set; } 28 | protected Dapper.SqlBuilder.Template QueryTemplate { get; set; } 29 | 30 | public SqlQueryContext(string from, dynamic parameters = null) 31 | { 32 | _splitOn = new List(); 33 | _types = new List(); 34 | Parameters = new DynamicParameters(parameters); 35 | SqlBuilder = new Dapper.SqlBuilder(); 36 | 37 | // See https://github.com/StackExchange/Dapper/blob/master/Dapper.SqlBuilder/SqlBuilder.cs 38 | QueryTemplate = SqlBuilder.AddTemplate($@"SELECT 39 | /**select**/ 40 | FROM {from}/**innerjoin**//**leftjoin**//**rightjoin**//**join**/ 41 | /**where**//**orderby**/"); 42 | } 43 | 44 | /// 45 | /// Adds a WHERE clause to the query, joining it with the previous with an 'AND' operator if needed. 46 | /// 47 | /// 48 | /// Do not include the 'WHERE' keyword, as it is added automatically. 49 | /// 50 | /// 51 | /// var queryBuilder = new SqlQueryBuilder(); 52 | /// queryBuilder.From("Customer customer"); 53 | /// queryBuilder.Select( 54 | /// "customer.id", 55 | /// "customer.name", 56 | /// ); 57 | /// queryBuilder.SplitOn("id"); 58 | /// queryBuilder.Where("customer.id == @id"); 59 | /// queryBuilder.Parameters.Add("id", 1); 60 | /// var customer = queryBuilder 61 | /// .Execute(dbConnection, graphQLSelectionSet); 62 | /// .FirstOrDefault(); 63 | /// 64 | /// // SELECT customer.id, customer.name 65 | /// // FROM Customer customer 66 | /// // WHERE customer.id == @id 67 | /// 68 | /// An array of WHERE clauses. 69 | /// The query builder. 70 | public SqlQueryContext AndWhere(string where, dynamic parameters = null) 71 | { 72 | Parameters.AddDynamicParams(parameters); 73 | SqlBuilder.Where(where); 74 | return this; 75 | } 76 | 77 | /// 78 | /// Executes the query with Dapper, using the provided database connection and map function. 79 | /// 80 | /// 81 | /// var queryBuilder = new SqlQueryBuilder(); 82 | /// queryBuilder.From("Customer customer"); 83 | /// queryBuilder.Select( 84 | /// "customer.id", 85 | /// "customer.name", 86 | /// ); 87 | /// queryBuilder.SplitOn("id"); 88 | /// queryBuilder.Where("customer.id == @id"); 89 | /// queryBuilder.Parameters.Add("id", 1); 90 | /// var customer = queryBuilder 91 | /// .Execute(dbConnection, graphQLSelectionSet) 92 | /// .FirstOrDefault(); 93 | /// 94 | /// // SELECT customer.id, customer.name 95 | /// // FROM Customer customer 96 | /// // WHERE customer.id == @id 97 | /// 98 | /// The entity type to be mapped. 99 | /// The database connection. 100 | /// The entity mapper. 101 | /// The GraphQL selection set (optional). 102 | /// The transaction to execute under (optional). 103 | /// The options for the query (optional). 104 | /// A list of entities returned by the query. 105 | public IEnumerable Execute( 106 | IDbConnection connection, 107 | IHaveSelectionSet selectionSet, 108 | IEntityMapper mapper = null, 109 | IDbTransaction transaction = null, 110 | SqlMapperOptions options = null) 111 | where TEntityType : class 112 | { 113 | if (options == null) { 114 | options = SqlMapperOptions.DefaultOptions; 115 | } 116 | 117 | if (mapper == null) 118 | { 119 | mapper = new EntityMapper(); 120 | } 121 | 122 | // Build function that uses a mapping context to map our entities 123 | var fn = new Func(objs => 124 | { 125 | var context = new EntityMapContext 126 | { 127 | Items = objs, 128 | SelectionSet = selectionSet, 129 | SplitOn = GetSplitOnTypes(), 130 | }; 131 | using (context) 132 | { 133 | return mapper.Map(context); 134 | } 135 | }); 136 | 137 | var results = connection.Query( 138 | sql: this.ToString(), 139 | types: this._types.ToArray(), 140 | param: this.Parameters, 141 | map: fn, 142 | splitOn: string.Join(",", this._splitOn), 143 | transaction: transaction, 144 | commandTimeout: options.CommandTimeout, 145 | commandType: options.CommandType, 146 | buffered: options.Buffered 147 | ); 148 | return results.Where(e => e != null); 149 | } 150 | 151 | 152 | /// 153 | /// Executes the query with Dapper asynchronously, using the provided database connection and map function. 154 | /// 155 | /// 156 | /// var queryBuilder = new SqlQueryBuilder(); 157 | /// queryBuilder.From("Customer customer"); 158 | /// queryBuilder.Select( 159 | /// "customer.id", 160 | /// "customer.name", 161 | /// ); 162 | /// queryBuilder.SplitOn("id"); 163 | /// queryBuilder.Where("customer.id == @id"); 164 | /// queryBuilder.Parameters.Add("id", 1); 165 | /// var customer = queryBuilder 166 | /// .Execute(dbConnection, graphQLSelectionSet) 167 | /// .FirstOrDefault(); 168 | /// 169 | /// // SELECT customer.id, customer.name 170 | /// // FROM Customer customer 171 | /// // WHERE customer.id == @id 172 | /// 173 | /// The entity type to be mapped. 174 | /// The database connection. 175 | /// The entity mapper. 176 | /// The GraphQL selection set (optional). 177 | /// The transaction to execute under (optional). 178 | /// The options for the query (optional). 179 | /// A list of entities returned by the query. 180 | public async Task> ExecuteAsync( 181 | IDbConnection connection, 182 | IHaveSelectionSet selectionSet, 183 | IEntityMapper mapper = null, 184 | IDbTransaction transaction = null, 185 | SqlMapperOptions options = null) 186 | where TEntityType : class 187 | { 188 | if (options == null) { 189 | options = SqlMapperOptions.DefaultOptions; 190 | } 191 | 192 | if (mapper == null) 193 | { 194 | mapper = new EntityMapper(); 195 | } 196 | 197 | // Build function that uses a mapping context to map our entities 198 | var fn = new Func(objs => 199 | { 200 | var context = new EntityMapContext 201 | { 202 | Items = objs, 203 | SelectionSet = selectionSet, 204 | SplitOn = GetSplitOnTypes(), 205 | }; 206 | using (context) 207 | { 208 | return mapper.Map(context); 209 | } 210 | }); 211 | 212 | var results = await connection.QueryAsync( 213 | sql: this.ToString(), 214 | types: this._types.ToArray(), 215 | param: this.Parameters, 216 | map: fn, 217 | splitOn: string.Join(",", this._splitOn), 218 | transaction: transaction, 219 | commandTimeout: options.CommandTimeout, 220 | commandType: options.CommandType, 221 | buffered: options.Buffered 222 | ); 223 | return results.Where(e => e != null); 224 | } 225 | 226 | /// 227 | /// Gets an array of types that are used to split objects during entity mapping. 228 | /// 229 | /// 230 | public List GetSplitOnTypes() 231 | { 232 | return _types; 233 | } 234 | 235 | /// 236 | /// Performs an INNER JOIN. 237 | /// 238 | /// 239 | /// Do not include the 'INNER JOIN' keywords, as they are added automatically. 240 | /// 241 | /// 242 | /// var queryBuilder = new SqlQueryBuilder(); 243 | /// queryBuilder.From("Customer customer"); 244 | /// queryBuilder.InnerJoin("Account account ON customer.Id = account.CustomerId"); 245 | /// queryBuilder.Select( 246 | /// "customer.id", 247 | /// "account.id", 248 | /// ); 249 | /// queryBuilder.SplitOn("id"); 250 | /// queryBuilder.SplitOn("id"); 251 | /// queryBuilder.Where("customer.id == @id"); 252 | /// queryBuilder.Parameters.Add("id", 1); 253 | /// var customer = queryBuilder 254 | /// .Execute(dbConnection, graphQLSelectionSet); 255 | /// .FirstOrDefault(); 256 | /// 257 | /// // SELECT customer.id, account.id 258 | /// // FROM 259 | /// // Customer customer INNER JOIN 260 | /// // Account account ON customer.Id = account.CustomerId 261 | /// // WHERE customer.id == @id 262 | /// 263 | /// The INNER JOIN clause. 264 | /// Parameters included in the statement. 265 | /// The query builder. 266 | public SqlQueryContext InnerJoin(string join, dynamic parameters = null) 267 | { 268 | RemoveSingleTableQueryItems(); 269 | 270 | Parameters.AddDynamicParams(parameters); 271 | SqlBuilder.InnerJoin(join); 272 | return this; 273 | } 274 | 275 | /// 276 | /// Performs a LEFT OUTER JOIN. 277 | /// 278 | /// 279 | /// Do not include the 'LEFT OUTER JOIN' keywords, as they are added automatically. 280 | /// 281 | /// 282 | /// var queryBuilder = new SqlQueryBuilder(); 283 | /// queryBuilder.From("Customer customer"); 284 | /// queryBuilder.LeftOuterJoin("Account account ON customer.Id = account.CustomerId"); 285 | /// queryBuilder.Select( 286 | /// "customer.id", 287 | /// "account.id", 288 | /// ); 289 | /// queryBuilder.SplitOn("id"); 290 | /// queryBuilder.SplitOn("id"); 291 | /// queryBuilder.Where("customer.id == @id"); 292 | /// queryBuilder.Parameters.Add("id", 1); 293 | /// var customer = queryBuilder 294 | /// .Execute(dbConnection, graphQLSelectionSet); 295 | /// .FirstOrDefault(); 296 | /// 297 | /// // SELECT customer.id, account.id 298 | /// // FROM 299 | /// // Customer customer LEFT OUTER JOIN 300 | /// // Account account ON customer.Id = account.CustomerId 301 | /// // WHERE customer.id == @id 302 | /// 303 | /// The LEFT JOIN clause. 304 | /// Parameters included in the statement. 305 | /// The query builder. 306 | public SqlQueryContext LeftJoin(string join, dynamic parameters = null) 307 | { 308 | RemoveSingleTableQueryItems(); 309 | 310 | Parameters.AddDynamicParams(parameters); 311 | SqlBuilder.LeftJoin(join); 312 | return this; 313 | } 314 | 315 | /// 316 | /// Adds an ORDER BY clause to the end of the query. 317 | /// 318 | /// 319 | /// Do not include the 'ORDER BY' keywords, as they are added automatically. 320 | /// 321 | /// 322 | /// var queryBuilder = new SqlQueryBuilder(); 323 | /// queryBuilder.From("Customer customer"); 324 | /// queryBuilder.Select( 325 | /// "customer.id", 326 | /// "customer.name", 327 | /// ); 328 | /// queryBuilder.SplitOn("id"); 329 | /// queryBuilder.Where("customer.id == @id"); 330 | /// queryBuilder.Parameters.Add("id", 1); 331 | /// queryBuilder.Orderby("customer.name"); 332 | /// var customer = queryBuilder 333 | /// .Execute(dbConnection, graphQLSelectionSet); 334 | /// .FirstOrDefault(); 335 | /// 336 | /// // SELECT customer.id, customer.name 337 | /// // FROM Customer customer 338 | /// // WHERE customer.id == @id 339 | /// // ORDER BY customer.name 340 | /// 341 | /// One or more GROUP BY clauses. 342 | /// Parameters included in the statement. 343 | /// The query builder. 344 | public SqlQueryContext OrderBy(string orderBy, dynamic parameters = null) 345 | { 346 | Parameters.AddDynamicParams(parameters); 347 | SqlBuilder.OrderBy(orderBy); 348 | return this; 349 | } 350 | 351 | /// 352 | /// Adds a WHERE clause to the query, joining it with the previous with an 'OR' operator if needed. 353 | /// 354 | /// 355 | /// Do not include the 'WHERE' keyword, as it is added automatically. 356 | /// 357 | /// An array of WHERE clauses. 358 | /// Parameters included in the statement. 359 | /// The query builder. 360 | public SqlQueryContext OrWhere(string where, dynamic parameters = null) 361 | { 362 | Parameters.AddDynamicParams(parameters); 363 | SqlBuilder.OrWhere(where); 364 | return this; 365 | } 366 | 367 | /// 368 | /// Adds a SELECT statement to the query, joining it with previous items already selected. 369 | /// 370 | /// 371 | /// Do not include the 'SELECT' keyword, as it is added automatically. 372 | /// 373 | /// 374 | /// var queryBuilder = new SqlQueryBuilder(); 375 | /// var customer = queryBuilder 376 | /// .From("Customer customer") 377 | /// .Select( 378 | /// "customer.id", 379 | /// "customer.name", 380 | /// ) 381 | /// .SplitOn("id") 382 | /// .Where("customer.id == @id") 383 | /// .WithParameter("id", 1) 384 | /// .Execute(dbConnection, graphQLSelectionSet); 385 | /// .FirstOrDefault(); 386 | /// 387 | /// // SELECT customer.id, customer.name 388 | /// // FROM Customer customer 389 | /// // WHERE customer.id == @id 390 | /// 391 | /// The column to select. 392 | /// Parameters included in the statement. 393 | /// The query builder. 394 | public SqlQueryContext Select(string select, dynamic parameters = null) 395 | { 396 | Parameters.AddDynamicParams(parameters); 397 | SqlBuilder.Select(select); 398 | return this; 399 | } 400 | 401 | public SqlQueryContext Select(params string[] select) 402 | { 403 | foreach (var s in select) 404 | { 405 | SqlBuilder.Select(s); 406 | } 407 | return this; 408 | } 409 | 410 | /// 411 | /// Instructs dapper to deserialized data into a different type, beginning with the specified column. 412 | /// 413 | /// The type to map data into. 414 | /// The name of the column to map into a different type. 415 | /// 416 | /// The query builder. 417 | public SqlQueryContext SplitOn(string columnName) 418 | { 419 | return SplitOn(columnName, typeof(TEntityType)); 420 | } 421 | 422 | /// 423 | /// Instructs dapper to deserialized data into a different type, beginning with the specified column. 424 | /// 425 | /// The name of the column to map into a different type. 426 | /// The type to map data into. 427 | /// 428 | /// The query builder. 429 | public SqlQueryContext SplitOn(string columnName, Type entityType) 430 | { 431 | RemoveSingleTableQueryItems(); 432 | 433 | _splitOn.Add(columnName); 434 | _types.Add(entityType); 435 | 436 | return this; 437 | } 438 | 439 | /// 440 | /// Renders the generated SQL statement. 441 | /// 442 | /// The rendered SQL statement. 443 | public override string ToString() 444 | { 445 | return QueryTemplate.RawSql; 446 | } 447 | 448 | /// 449 | /// An alias for AndWhere(). 450 | /// 451 | /// The WHERE clause. 452 | /// Parameters included in the statement. 453 | public SqlQueryContext Where(string where, dynamic parameters = null) 454 | { 455 | return AndWhere(where, parameters); 456 | } 457 | 458 | /// 459 | /// Clears out items that are only relevant for single-table queries. 460 | /// 461 | private void RemoveSingleTableQueryItems() 462 | { 463 | if (_types.Count > 0 && _splitOn.Count == 0) 464 | { 465 | _types.Clear(); 466 | } 467 | } 468 | } 469 | } -------------------------------------------------------------------------------- /Dapper.GraphQL/Contexts/SqlUpdateContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Data; 4 | using System.Linq; 5 | using System.Reflection; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace Dapper.GraphQL 10 | { 11 | public class SqlUpdateContext 12 | { 13 | private HashSet UpdateParameterNames; 14 | public DynamicParameters Parameters { get; set; } 15 | private Dapper.SqlBuilder SqlBuilder { get; set; } 16 | public string Table { get; private set; } 17 | private Dapper.SqlBuilder.Template Template { get; set; } 18 | 19 | public SqlUpdateContext( 20 | string table, 21 | dynamic parameters = null) 22 | { 23 | if (parameters != null && !(parameters is IEnumerable>)) 24 | { 25 | parameters = ParameterHelper.GetSetFlatProperties(parameters); 26 | } 27 | this.Parameters = new DynamicParameters(parameters); 28 | this.SqlBuilder = new Dapper.SqlBuilder(); 29 | this.Table = table; 30 | this.Template = SqlBuilder.AddTemplate(@" 31 | /**where**/"); 32 | this.UpdateParameterNames = new HashSet(Parameters.ParameterNames); 33 | } 34 | 35 | /// 36 | /// Adds a WHERE clause to the query, joining it with the previous with an 'AND' operator if needed. 37 | /// 38 | /// 39 | /// Do not include the 'WHERE' keyword, as it is added automatically. 40 | /// 41 | /// 42 | /// SqlBuilder 43 | /// .Update("Person") 44 | /// .Where("Id = @id", new { id }) 45 | /// .Select("Id") 46 | /// .Select("Name") 47 | /// var queryBuilder = new SqlQueryBuilder(); 48 | /// queryBuilder.From("Customer customer"); 49 | /// queryBuilder.Select( 50 | /// "customer.id", 51 | /// "customer.name", 52 | /// ); 53 | /// queryBuilder.SplitOn("id"); 54 | /// queryBuilder.Where("customer.id == @id"); 55 | /// queryBuilder.Parameters.Add("id", 1); 56 | /// var customer = queryBuilder 57 | /// // Execute using the database connection, and providing the primary key 58 | /// // used to split entities. 59 | /// .Execute(dbConnection, customer => customer.Id); 60 | /// .FirstOrDefault(); 61 | /// 62 | /// // SELECT customer.id, customer.name 63 | /// // FROM Customer customer 64 | /// // WHERE customer.id == @id 65 | /// 66 | /// An array of WHERE clauses. 67 | /// Parameters included in the statement. 68 | /// The query builder. 69 | public SqlUpdateContext AndWhere(string where, dynamic parameters = null) 70 | { 71 | Parameters.AddDynamicParams(parameters); 72 | SqlBuilder.Where(where, parameters); 73 | return this; 74 | } 75 | 76 | /// 77 | /// Executes the update statement with Dapper, using the provided database connection. 78 | /// 79 | /// The database connection. 80 | /// The transaction to execute under (optional). 81 | /// The options for the command (optional). 82 | public int Execute(IDbConnection connection, IDbTransaction transaction = null, SqlMapperOptions options = null) 83 | { 84 | if (options == null) { 85 | options = SqlMapperOptions.DefaultOptions; 86 | } 87 | 88 | var result = connection.Execute(BuildSql(), Parameters, transaction, options.CommandTimeout, options.CommandType); 89 | return result; 90 | } 91 | 92 | /// 93 | /// Executes the update statement with Dapper asynchronously, using the provided database connection. 94 | /// 95 | /// The database connection. 96 | /// The transaction to execute under (optional). 97 | /// The options for the command (optional). 98 | public async Task ExecuteAsync(IDbConnection connection, IDbTransaction transaction = null, SqlMapperOptions options = null) 99 | { 100 | if (options == null) { 101 | options = SqlMapperOptions.DefaultOptions; 102 | } 103 | 104 | var result = await connection.ExecuteAsync(BuildSql(), Parameters, transaction, options.CommandTimeout, options.CommandType); 105 | return result; 106 | } 107 | 108 | /// 109 | /// Adds a WHERE clause to the query, joining it with the previous with an 'OR' operator if needed. 110 | /// 111 | /// 112 | /// Do not include the 'WHERE' keyword, as it is added automatically. 113 | /// 114 | /// A WHERE clause. 115 | /// Parameters included in the statement. 116 | /// The query builder. 117 | public SqlUpdateContext OrWhere(string where, dynamic parameters = null) 118 | { 119 | Parameters.AddDynamicParams(parameters); 120 | SqlBuilder.OrWhere(where, parameters); 121 | return this; 122 | } 123 | 124 | /// 125 | /// Renders the generated SQL statement. 126 | /// 127 | /// The rendered SQL statement. 128 | public override string ToString() 129 | { 130 | return BuildSql(); 131 | } 132 | 133 | /// 134 | /// An alias for AndWhere(). 135 | /// 136 | /// A WHERE clause. 137 | /// Parameters included in the statement. 138 | public SqlUpdateContext Where(string where, dynamic parameters = null) 139 | { 140 | Parameters.AddDynamicParams(parameters); 141 | SqlBuilder.Where(where); 142 | return this; 143 | } 144 | 145 | /// 146 | /// Builds the UPDATE statement. 147 | /// 148 | /// A SQL UPDATE statement. 149 | private string BuildSql() 150 | { 151 | var sb = new StringBuilder(); 152 | sb.Append($"UPDATE {Table} SET "); 153 | sb.Append(string.Join(", ", UpdateParameterNames.Select(name => $"{name} = @{name}"))); 154 | sb.Append(Template.RawSql); 155 | return sb.ToString(); 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /Dapper.GraphQL/Dapper.GraphQL.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard1.3 5 | 0.4.2-beta 6 | Doug Day, Kevin Russon, Ben McCallum, Natalya Arbit, Per Liedman, John Stovin 7 | Landmark Home Warranty 8 | A library designed to integrate the Dapper and graphql-dotnet projects with ease-of-use in mind and performance as the primary concern. 9 | Copyright 2018 10 | https://opensource.org/licenses/MIT 11 | https://github.com/landmarkhw/Dapper.GraphQL 12 | https://github.com/landmarkhw/Dapper.GraphQL.git 13 | git 14 | Dapper Dapper.SqlBuilder GraphQL graphql-dotnet core coreclr .NET 15 | Resolved 16 | #15 - Pass commandTimeout to Dapper 17 | #31 - Add non-generic versions of service registration methods 18 | #34 - SqlQueryContext: SplitOn() should call RemoveSingleTableQueryItems() 19 | 20 | 0.4.2.0 21 | 0.4.2.0 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /Dapper.GraphQL/DapperGraphQLOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using GraphQL; 4 | using GraphQL.Types; 5 | using Microsoft.Extensions.DependencyInjection; 6 | 7 | namespace Dapper.GraphQL 8 | { 9 | /// 10 | /// Options used to configure the dependency injection container for GraphQL and Dapper. 11 | /// 12 | public class DapperGraphQLOptions 13 | { 14 | private readonly IServiceCollection serviceCollection; 15 | 16 | public DapperGraphQLOptions(IServiceCollection serviceCollection) 17 | { 18 | this.serviceCollection = serviceCollection; 19 | } 20 | 21 | /// 22 | /// Adds a GraphQL query builder to the container. 23 | /// 24 | /// The model type to be queried. 25 | /// The query builder class. 26 | /// The GraphQLOptions object. 27 | public DapperGraphQLOptions AddQueryBuilder() 28 | where TQueryBuilder : class, IQueryBuilder 29 | { 30 | serviceCollection.AddSingleton, TQueryBuilder>(); 31 | return this; 32 | } 33 | 34 | /// 35 | /// Adds a GraphQL query builder to the container. 36 | /// 37 | /// The model type to be queried. 38 | /// The query builder class, must implement IQueryBuilder 39 | /// The GraphQLOptions object. 40 | public DapperGraphQLOptions AddQueryBuilder(Type modelType, Type queryBuilderType) 41 | { 42 | var queryBuilderInterface = typeof(IQueryBuilder<>).MakeGenericType(modelType); 43 | if (!queryBuilderType.IsConcrete() || !queryBuilderInterface.IsAssignableFrom(queryBuilderType)) 44 | { 45 | throw new ArgumentException($"QueryBuilder type must be concrete and implement IQueryBuilder<{modelType.Name}>."); 46 | } 47 | 48 | serviceCollection.Add(new ServiceDescriptor(queryBuilderInterface, queryBuilderType, ServiceLifetime.Singleton)); 49 | return this; 50 | } 51 | 52 | /// 53 | /// Adds a GraphQL schema to the container. 54 | /// 55 | /// The schema type to be mapped. 56 | /// The GraphQLOptions object. 57 | public DapperGraphQLOptions AddSchema() where TGraphSchema : class, ISchema 58 | { 59 | serviceCollection.AddSingleton(); 60 | return this; 61 | } 62 | 63 | /// 64 | /// Adds a GraphQL schema to the container. 65 | /// 66 | /// The schema type to be mapped, must implement ISchema. 67 | /// The GraphQLOptions object. 68 | public DapperGraphQLOptions AddSchema(Type graphSchemaType) 69 | { 70 | if (!graphSchemaType.IsConcrete() || !typeof(ISchema).IsAssignableFrom(graphSchemaType)) 71 | { 72 | throw new ArgumentException("Type must be concrete and implement ISchema."); 73 | } 74 | 75 | serviceCollection.Add(new ServiceDescriptor(graphSchemaType, graphSchemaType, ServiceLifetime.Singleton)); 76 | return this; 77 | } 78 | 79 | /// 80 | /// Adds a GraphQL type to the container. 81 | /// 82 | /// The model type to be mapped. 83 | /// The GraphQLOptions object. 84 | public DapperGraphQLOptions AddType() where TGraphType : class, IGraphType 85 | { 86 | serviceCollection.AddSingleton(); 87 | return this; 88 | } 89 | 90 | /// 91 | /// Adds a GraphQL type to the container. 92 | /// 93 | /// The model type to be mapped, must implement IGraphType. 94 | /// The GraphQLOptions object. 95 | public DapperGraphQLOptions AddType(Type type) 96 | { 97 | if (!type.IsConcrete() || !typeof(IGraphType).IsAssignableFrom(type)) 98 | { 99 | throw new ArgumentException("Type must be concrete and implement IGraphType."); 100 | } 101 | 102 | serviceCollection.Add(new ServiceDescriptor(type, type, ServiceLifetime.Singleton)); 103 | return this; 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Dapper.GraphQL/DeduplicatingEntityMapper.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Language.AST; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | 7 | namespace Dapper.GraphQL 8 | { 9 | /// 10 | /// An entity mapper that deduplicates as it maps. 11 | /// 12 | /// 13 | /// The first entity found for each PrimaryKey is the object reference that will be returned. All 14 | /// other entities that match the primary key will be ignored. 15 | /// 16 | public abstract class DeduplicatingEntityMapper : 17 | IEntityMapper 18 | where TEntityType : class 19 | { 20 | /// 21 | /// Sets a function that returns the primary key used to uniquely identify the entity. 22 | /// 23 | public Func PrimaryKey { get; set; } 24 | 25 | /// 26 | /// A cache used to hold previous entities that this mapper has seen. 27 | /// 28 | protected IDictionary KeyCache { get; set; } = new Dictionary(); 29 | 30 | /// 31 | /// Maps a row of data to an entity. 32 | /// 33 | /// A context that contains information used to map Dapper objects. 34 | /// The mapped entity, or null if the entity has previously been returned. 35 | public abstract TEntityType Map(EntityMapContext context); 36 | 37 | /// 38 | /// Resolves the deduplicated entity. 39 | /// 40 | /// The entity to deduplicate. 41 | /// The deduplicated entity. 42 | protected virtual TEntityType Deduplicate(TEntityType entity) 43 | { 44 | if (entity == default(TEntityType)) 45 | { 46 | return default(TEntityType); 47 | } 48 | 49 | if (PrimaryKey == null) 50 | { 51 | throw new InvalidOperationException("PrimaryKey selector is not defined, but is required to use DeduplicatingEntityMapper."); 52 | } 53 | 54 | var previous = entity; 55 | 56 | // Get the primary key for this entity 57 | var primaryKey = PrimaryKey(entity); 58 | if (primaryKey == null) 59 | { 60 | throw new InvalidOperationException("A null primary key was provided, which results in an unpredictable state."); 61 | } 62 | 63 | // Deduplicate the entity using available information 64 | if (KeyCache.ContainsKey(primaryKey)) 65 | { 66 | // Get the duplicate entity 67 | entity = KeyCache[primaryKey]; 68 | } 69 | else 70 | { 71 | // Cache a reference to the entity 72 | KeyCache[primaryKey] = entity; 73 | } 74 | return entity; 75 | } 76 | } 77 | } -------------------------------------------------------------------------------- /Dapper.GraphQL/EntityMapper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Dapper.GraphQL 6 | { 7 | public class EntityMapper : 8 | IEntityMapper 9 | where TEntityType : class 10 | { 11 | public virtual TEntityType Map(EntityMapContext context) 12 | { 13 | var entity = context.Start(); 14 | return entity; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Dapper.GraphQL/Extensions/EntityMapContextExtensions.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Language.AST; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | 7 | namespace Dapper.GraphQL 8 | { 9 | public static class EntityMapContextExtensions 10 | { 11 | /// 12 | /// Maps the next object from Dapper. 13 | /// 14 | /// The item type to be mapped. 15 | /// The context used to map object from Dapper. 16 | /// The name of the GraphQL field associated with the item. 17 | /// An optional entity mapper. This is used to map complex objects from Dapper mapping results. 18 | /// The mapped item. 19 | public static TItemType Next( 20 | this EntityMapContext context, 21 | string fieldName, 22 | IEntityMapper entityMapper = null) 23 | where TItemType : class 24 | { 25 | return context.Next( 26 | new[] { fieldName }, 27 | (currentSelectionSet, selectionSet) => currentSelectionSet[fieldName], 28 | entityMapper 29 | ); 30 | } 31 | 32 | /// 33 | /// Maps the next object from Dapper, from a list of fields. 34 | /// 35 | /// The item type to be mapped. 36 | /// The context used to map object from Dapper. 37 | /// The GraphQL fields associated with the item. 38 | /// An optional entity mapper. This is used to map complex objects from Dapper mapping results. 39 | /// The mapped item. 40 | public static TItemType Next( 41 | this EntityMapContext context, 42 | IEnumerable fieldNames, 43 | IEntityMapper entityMapper = null) 44 | where TItemType : class 45 | { 46 | return context.Next( 47 | fieldNames, 48 | (currentSelectionSet, selectionSet) => selectionSet, 49 | entityMapper 50 | ); 51 | } 52 | 53 | /// 54 | /// Maps the next object from an inline fragment. 55 | /// 56 | /// The item type to be mapped. 57 | /// The context used to map object from Dapper. 58 | /// The GraphQL field that contains the inline fragment(s). 59 | /// An optional entity mapper. This is used to map complex objects from Dapper mapping results. 60 | /// The mapped item. 61 | public static TItemType NextFragment( 62 | this EntityMapContext context, 63 | string fieldName, 64 | IEntityMapper entityMapper = null) 65 | where TItemType : class 66 | { 67 | return context.Next( 68 | new[] { fieldName }, 69 | (currentSelectionSet, selectionSet) => currentSelectionSet[fieldName] 70 | .SelectionSet 71 | .Selections 72 | .OfType() 73 | .Where(f => f.Type.Name == typeof(TItemType).Name) 74 | .FirstOrDefault(), 75 | entityMapper 76 | ); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Dapper.GraphQL/Extensions/IEnumerable`Extensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | 5 | namespace Dapper.GraphQL 6 | { 7 | public static class IEnumerable_Extensions 8 | { 9 | /// 10 | /// An async version of the Aggregate with seed method found in dotnet/corefx 11 | /// https://github.com/dotnet/corefx/blob/master/src/System.Linq/src/System/Linq/Aggregate.cs 12 | /// 13 | /// 14 | /// 15 | /// 16 | /// 17 | /// 18 | /// 19 | public static async Task AggregateAsync(this IEnumerable source, TAccumulate seed, Func> funcAsync) 20 | { 21 | if (source == null) 22 | { 23 | throw new ArgumentNullException(nameof(source)); 24 | } 25 | 26 | if (funcAsync == null) 27 | { 28 | throw new ArgumentNullException(nameof(funcAsync)); 29 | } 30 | 31 | TAccumulate result = seed; 32 | foreach (TSource element in source) 33 | { 34 | result = await funcAsync(result, element); 35 | } 36 | 37 | return result; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Dapper.GraphQL/Extensions/IHaveSelectionSetExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace GraphQL.Language.AST 5 | { 6 | public static class IHaveSelectionSetExtensions 7 | { 8 | /// 9 | /// Returns a map of selected fields, keyed by the field name. 10 | /// 11 | /// The GraphQL selection set container. 12 | /// A dictionary whose key is the field name, and value is the field contents. 13 | public static IDictionary GetSelectedFields(this IHaveSelectionSet selectionSet) 14 | { 15 | if (selectionSet != null) 16 | { 17 | var fields = selectionSet 18 | .SelectionSet 19 | .Selections 20 | .OfType() 21 | .ToDictionary(field => field.Name); 22 | 23 | return fields; 24 | } 25 | return null; 26 | } 27 | 28 | /// 29 | /// Returns the inline fragment for the specified entity within the GraphQL selection. 30 | /// 31 | /// The type of entity to retrieve. 32 | /// The GraphQL selection set. 33 | /// The inline framgent associated with the entity. 34 | public static InlineFragment GetInlineFragment(this IHaveSelectionSet selectionSet) 35 | { 36 | return selectionSet 37 | .SelectionSet? 38 | .Selections? 39 | .OfType() 40 | .Where(f => f.Type?.Name == typeof(TEntityType).Name) 41 | .FirstOrDefault(); 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /Dapper.GraphQL/Extensions/PostgreSql.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Data; 4 | using System.Linq; 5 | using System.Linq.Expressions; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace Dapper.GraphQL 10 | { 11 | public static class PostgreSql 12 | { 13 | public static TIdentityType NextIdentity(IDbConnection dbConnection, Expression> identityNameSelector) 14 | where TEntityType : class 15 | { 16 | if (identityNameSelector.Body.NodeType != ExpressionType.MemberAccess) 17 | { 18 | throw new NotSupportedException("Cannot execute a PostgreSQL identity with an expression of type " + identityNameSelector.Body.NodeType); 19 | } 20 | var memberExpression = identityNameSelector.Body as MemberExpression; 21 | 22 | var sb = new StringBuilder(); 23 | sb.AppendLine($"SELECT nextval(pg_get_serial_sequence('{typeof(TEntityType).Name.ToLower()}', '{memberExpression.Member.Name.ToLower()}'));"); 24 | 25 | return dbConnection 26 | .Query(sb.ToString()) 27 | .Single(); 28 | } 29 | 30 | public static async Task NextIdentityAsync(IDbConnection dbConnection, Expression> identityNameSelector) 31 | where TEntityType : class 32 | { 33 | if (identityNameSelector.Body.NodeType != ExpressionType.MemberAccess) 34 | { 35 | throw new NotSupportedException("Cannot execute a PostgreSQL identity with an expression of type " + identityNameSelector.Body.NodeType); 36 | } 37 | var memberExpression = identityNameSelector.Body as MemberExpression; 38 | 39 | var sb = new StringBuilder(); 40 | sb.AppendLine($"SELECT nextval(pg_get_serial_sequence('{typeof(TEntityType).Name.ToLower()}', '{memberExpression.Member.Name.ToLower()}'));"); 41 | 42 | var result = await dbConnection.QueryAsync(sb.ToString()); 43 | return result.Single(); 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /Dapper.GraphQL/Extensions/ServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using System; 3 | 4 | namespace Dapper.GraphQL 5 | { 6 | public static class ServiceCollectionExtensions 7 | { 8 | /// 9 | /// Initializes Dapper and GraphQL with the dependency injection container. 10 | /// 11 | /// The service collection container. 12 | /// An action used to initialize Dapper and GraphQL with the DI container. 13 | /// The service collection container. 14 | public static IServiceCollection AddDapperGraphQL(this IServiceCollection serviceCollection, Action setup) 15 | { 16 | var options = new DapperGraphQLOptions(serviceCollection); 17 | setup(options); 18 | 19 | return serviceCollection; 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /Dapper.GraphQL/Extensions/SqlInsertContextExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Data; 3 | using System.Linq; 4 | using System.Linq.Expressions; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace Dapper.GraphQL 9 | { 10 | public static class SqlInsertContextExtensions 11 | { 12 | public static TIdentityType ExecuteWithPostgreSqlIdentity(this SqlInsertContext context, IDbConnection dbConnection, Expression> identityNameSelector) 13 | where TEntityType : class 14 | { 15 | if (identityNameSelector.Body.NodeType != ExpressionType.MemberAccess) 16 | { 17 | throw new NotSupportedException("Cannot execute a PostgreSQL identity with an expression of type " + identityNameSelector.Body.NodeType); 18 | } 19 | var memberExpression = identityNameSelector.Body as MemberExpression; 20 | 21 | var sb = BuildPostgreSqlIdentityQuery(context, memberExpression.Member.Name.ToLower()); 22 | 23 | return dbConnection 24 | .Query(sb.ToString(), context.Parameters) 25 | .Single(); 26 | } 27 | 28 | public static async Task ExecuteWithPostgreSqlIdentityAsync(this SqlInsertContext context, IDbConnection dbConnection, Expression> identityNameSelector) 29 | where TEntityType : class 30 | { 31 | if (identityNameSelector.Body.NodeType != ExpressionType.MemberAccess) 32 | { 33 | throw new NotSupportedException("Cannot execute a PostgreSQL identity with an expression of type " + identityNameSelector.Body.NodeType); 34 | } 35 | var memberExpression = identityNameSelector.Body as MemberExpression; 36 | 37 | var sb = BuildPostgreSqlIdentityQuery(context, memberExpression.Member.Name.ToLower()); 38 | 39 | var result = await dbConnection.QueryAsync(sb.ToString(), context.Parameters); 40 | return result.Single(); 41 | } 42 | 43 | public static TIdentityType ExecuteWithSqlIdentity(this SqlInsertContext context, IDbConnection dbConnection, Func identityTypeSelector) 44 | where TEntityType : class 45 | { 46 | return ExecuteWithSqlIdentity(context, dbConnection); 47 | } 48 | 49 | public static TIdentityType ExecuteWithSqlIdentity(this SqlInsertContext context, IDbConnection dbConnection) 50 | { 51 | var sb = BuildSqlIdentityQuery(context); 52 | 53 | return dbConnection 54 | .Query(sb.ToString(), context.Parameters) 55 | .Single(); 56 | } 57 | 58 | public static async Task ExecuteWithSqlIdentityAsync(this SqlInsertContext context, IDbConnection dbConnection, Func identityTypeSelector) 59 | where TEntityType : class 60 | { 61 | return await ExecuteWithSqlIdentityAsync(context, dbConnection); 62 | } 63 | 64 | public static async Task ExecuteWithSqlIdentityAsync(this SqlInsertContext context, IDbConnection dbConnection) 65 | { 66 | var sb = BuildSqlIdentityQuery(context); 67 | 68 | var task = dbConnection 69 | .QueryAsync(sb.ToString(), context.Parameters); 70 | return (await task).Single(); 71 | } 72 | 73 | public static int ExecuteWithSqliteIdentity(this SqlInsertContext context, IDbConnection dbConnection) 74 | { 75 | var sb = BuildSqliteIdentityQuery(context); 76 | 77 | return dbConnection 78 | .Query(sb.ToString(), context.Parameters) 79 | .Single(); 80 | } 81 | 82 | public static async Task ExecuteWithSqliteIdentityAsync(this SqlInsertContext context, IDbConnection dbConnection) 83 | { 84 | var sb = BuildSqliteIdentityQuery(context); 85 | 86 | var task = dbConnection 87 | .QueryAsync(sb.ToString(), context.Parameters); 88 | return (await task).Single(); 89 | } 90 | 91 | private static StringBuilder BuildPostgreSqlIdentityQuery(SqlInsertContext context, string idName) 92 | { 93 | var sb = new StringBuilder(); 94 | sb.AppendLine(context.ToString()); 95 | sb.AppendLine($"SELECT currval(pg_get_serial_sequence('{context.Table.ToLower()}', '{idName.ToLower()}'));"); 96 | return sb; 97 | } 98 | 99 | private static StringBuilder BuildSqlIdentityQuery(SqlInsertContext context) 100 | { 101 | var sb = new StringBuilder(); 102 | 103 | if (typeof(TIdentityType) == typeof(int)) 104 | { 105 | sb.AppendLine(context.ToString()); 106 | sb.AppendLine("SELECT CAST(SCOPE_IDENTITY() AS INT)"); 107 | } 108 | else if (typeof(TIdentityType) == typeof(long)) 109 | { 110 | sb.AppendLine(context.ToString()); 111 | sb.AppendLine("SELECT CAST(SCOPE_IDENTITY() AS BIGINT)"); 112 | } 113 | else throw new InvalidCastException($"Type {typeof(TIdentityType).Name} in not supported this SQL context."); 114 | 115 | return sb; 116 | } 117 | 118 | private static StringBuilder BuildSqliteIdentityQuery(SqlInsertContext context) 119 | { 120 | var sb = new StringBuilder(); 121 | 122 | sb.AppendLine(context.ToString()); 123 | sb.AppendLine("SELECT last_insert_rowid();"); 124 | 125 | return sb; 126 | } 127 | } 128 | } -------------------------------------------------------------------------------- /Dapper.GraphQL/Interfaces/IEntityMapper.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Language.AST; 2 | using System; 3 | using System.Collections.Generic; 4 | 5 | namespace Dapper.GraphQL 6 | { 7 | /// 8 | /// Maps a row of objects from Dapper into an entity. 9 | /// 10 | /// The type of entity to be mapped. 11 | public interface IEntityMapper where TEntityType : class 12 | { 13 | /// 14 | /// Maps a row of data to an entity. 15 | /// 16 | /// A context that contains information used to map Dapper objects. 17 | /// The mapped entity, or null if the entity has previously been returned. 18 | TEntityType Map(EntityMapContext context); 19 | } 20 | } -------------------------------------------------------------------------------- /Dapper.GraphQL/Interfaces/IQueryBuilder.cs: -------------------------------------------------------------------------------- 1 | using Dapper.GraphQL; 2 | using GraphQL.Language.AST; 3 | 4 | namespace Dapper.GraphQL 5 | { 6 | /// 7 | /// Builds queries for a given entity type. 8 | /// 9 | /// The type of entity for which to build a query. 10 | public interface IQueryBuilder 11 | { 12 | /// 13 | /// Builds a query using a baseline query, the GraphQL context, and the current table alias. 14 | /// 15 | /// The query to augment with additional information/data. 16 | /// The GraphQL selection set for the area being built. 17 | /// The alias of the entity within the query to use. 18 | /// A query for the given type. 19 | SqlQueryContext Build(SqlQueryContext query, IHaveSelectionSet context, string alias); 20 | } 21 | } -------------------------------------------------------------------------------- /Dapper.GraphQL/ParameterHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Reflection; 6 | 7 | namespace Dapper.GraphQL 8 | { 9 | public static class ParameterHelper 10 | { 11 | private static Dictionary PropertyCache = new Dictionary(); 12 | private static Dictionary TypeInfoCache = new Dictionary(); 13 | 14 | /// 15 | /// Gets a list of flat properties that have been set on the object. 16 | /// 17 | /// The type to get properties from. 18 | /// The object to get properties from. 19 | /// A list of key-value pairs of property names and values. 20 | public static IEnumerable> GetSetFlatProperties(TType obj) 21 | { 22 | var type = obj.GetType(); 23 | PropertyInfo[] properties; 24 | if (!PropertyCache.ContainsKey(type)) 25 | { 26 | lock (PropertyCache) 27 | { 28 | if (!PropertyCache.ContainsKey(type)) 29 | { 30 | // Get a list of properties that are "flat" on this object, i.e. singular values 31 | properties = type 32 | .GetProperties() 33 | .Where(p => 34 | { 35 | var typeInfo = GetTypeInfo(p.PropertyType); 36 | 37 | // Explicitly permit primitive, value, and serializable types 38 | if (typeInfo.IsSerializable || typeInfo.IsPrimitive || typeInfo.IsValueType) 39 | { 40 | return true; 41 | } 42 | 43 | // Filter out list-types 44 | if (typeof(IEnumerable).IsAssignableFrom(p.PropertyType)) 45 | { 46 | return false; 47 | } 48 | if (p.PropertyType.IsConstructedGenericType) 49 | { 50 | var typeDef = p.PropertyType.GetGenericTypeDefinition(); 51 | if (typeof(IEnumerable<>).IsAssignableFrom(typeDef) || 52 | typeof(ICollection<>).IsAssignableFrom(typeDef) || 53 | typeof(IList<>).IsAssignableFrom(typeDef)) 54 | { 55 | return false; 56 | } 57 | } 58 | return true; 59 | }) 60 | .ToArray(); 61 | 62 | // Cache those properties 63 | PropertyCache[type] = properties; 64 | } 65 | else 66 | { 67 | properties = PropertyCache[type]; 68 | } 69 | } 70 | } 71 | else 72 | { 73 | properties = PropertyCache[type]; 74 | } 75 | 76 | // Convert the properties to a dictionary where: 77 | // Key = property name 78 | // Value = property value, or null if the property is set to its default value 79 | return properties 80 | .ToDictionary( 81 | prop => prop.Name, 82 | prop => 83 | { 84 | // Ensure scalar values are properly skipped if they are set to their initial, default(type) value. 85 | var value = prop.GetValue(obj); 86 | if (value != null) 87 | { 88 | var valueType = value.GetType(); 89 | var valueTypeInfo = GetTypeInfo(valueType); 90 | if (valueTypeInfo.IsValueType && 91 | object.Equals(value, Activator.CreateInstance(valueType))) 92 | { 93 | return null; 94 | } 95 | } 96 | return value; 97 | } 98 | ) 99 | // Then, filter out "unset" properties, or properties that are set to their default value 100 | .Where(kvp => kvp.Value != null); 101 | } 102 | 103 | private static TypeInfo GetTypeInfo(Type type) 104 | { 105 | if (!TypeInfoCache.ContainsKey(type)) 106 | { 107 | lock (TypeInfoCache) 108 | { 109 | if (!TypeInfoCache.ContainsKey(type)) 110 | { 111 | TypeInfoCache[type] = type.GetTypeInfo(); 112 | } 113 | } 114 | } 115 | return TypeInfoCache[type]; 116 | } 117 | } 118 | } -------------------------------------------------------------------------------- /Dapper.GraphQL/Properties/PublishProfiles/FolderProfile.pubxml: -------------------------------------------------------------------------------- 1 |  2 | 5 | 6 | 7 | FileSystem 8 | Release 9 | netstandard1.3 10 | bin\Release\PublishOutput 11 | Any CPU 12 | 13 | -------------------------------------------------------------------------------- /Dapper.GraphQL/SqlBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Data; 4 | using System.Data.Common; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Text.RegularExpressions; 8 | 9 | namespace Dapper.GraphQL 10 | { 11 | /// 12 | /// A builder for SQL queries and statements. 13 | /// 14 | public static class SqlBuilder 15 | { 16 | public static SqlDeleteContext Delete(string from, dynamic parameters = null) 17 | { 18 | return new SqlDeleteContext(from, parameters); 19 | } 20 | 21 | public static SqlDeleteContext Delete(dynamic parameters = null) 22 | where TEntityType : class 23 | { 24 | return new SqlDeleteContext(typeof(TEntityType).Name, parameters); 25 | } 26 | 27 | public static SqlQueryContext From(string from, dynamic parameters = null) 28 | { 29 | return new SqlQueryContext(from, parameters); 30 | } 31 | 32 | public static SqlQueryContext From(string alias = null) 33 | where TEntityType : class 34 | { 35 | return new SqlQueryContext(alias); 36 | } 37 | 38 | public static SqlInsertContext Insert(TEntityType obj) 39 | where TEntityType : class 40 | { 41 | return new SqlInsertContext(typeof(TEntityType).Name, obj); 42 | } 43 | 44 | public static SqlInsertContext Insert(string table, dynamic parameters = null) 45 | { 46 | return new SqlInsertContext(table, parameters); 47 | } 48 | 49 | public static SqlUpdateContext Update(TEntityType obj) 50 | where TEntityType : class 51 | { 52 | return new SqlUpdateContext(typeof(TEntityType).Name, obj); 53 | } 54 | 55 | public static SqlUpdateContext Update(string table, dynamic parameters = null) 56 | { 57 | return new SqlUpdateContext(table, parameters); 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /Dapper.GraphQL/SqlOptions.cs: -------------------------------------------------------------------------------- 1 | using System.Data; 2 | 3 | namespace Dapper.GraphQL 4 | { 5 | /// 6 | /// Options for database queries and commands. 7 | /// These options corresponds to the optional parameters used by 8 | /// Dapper's SqlMapper class. 9 | /// 10 | public class SqlMapperOptions { 11 | /// 12 | /// The default options used unless other are specified. 13 | /// 14 | public static readonly SqlMapperOptions DefaultOptions = new SqlMapperOptions(); 15 | 16 | /// 17 | /// Number of seconds before command execution timeout. 18 | /// 19 | public int? CommandTimeout { get; set; } 20 | 21 | /// 22 | /// Is it a stored proc or a batch? 23 | /// 24 | public CommandType? CommandType { get; set; } 25 | 26 | /// 27 | /// Whether to buffer the results in memory. 28 | /// 29 | public bool Buffered { get; set; } = true; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Landmark Home Warranty 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dapper.GraphQL 2 | A library designed to integrate the Dapper and graphql-dotnet projects with ease-of-use in mind and performance as the primary concern. 3 | 4 | # Design 5 | Dapper.GraphQL combines the ideas that come out-of-the-box with graphql-dotnet, and adds the following concepts: 6 | 7 | 1. Query builders 8 | 2. Entity mapper 9 | 10 | ## Query Builders 11 | 12 | Query builders are used to build dynamic queries based on what the client asked for in their GraphQL query. For example, given this 13 | GraphQL query: 14 | 15 | ```graphql 16 | query { 17 | people { 18 | id 19 | firstName 20 | lastName 21 | } 22 | } 23 | ``` 24 | 25 | A proper query builder will generate a SQL query that looks something like this: 26 | 27 | ```sql 28 | SELECT id, firstName, lastName 29 | FROM Person 30 | ``` 31 | 32 | Using the same query builder, and given the following GraphQL: 33 | 34 | ```graphql 35 | query { 36 | people { 37 | id 38 | firstName 39 | lastName 40 | emails { 41 | id 42 | address 43 | } 44 | phones { 45 | id 46 | number 47 | type 48 | } 49 | } 50 | } 51 | ``` 52 | 53 | A more complex query should be generated, something like: 54 | 55 | ```sql 56 | SELECT 57 | person.Id, person.firstName, person.lastName, 58 | email.id, email.address, 59 | phone.id, phone.number, phone.type 60 | FROM Person person 61 | LEFT OUTER JOIN Email email ON person.Id = email.PersonId 62 | LEFT OUTER JOIN Phone phone ON person.Id = phone.PersonId 63 | ``` 64 | 65 | ## Entity Mappers 66 | 67 | Entity mappers are used to map entities to Dapper from query results. Since a single entity can be composed of multiple rows of a query result, an entity mapper is designed to quickly merge multiple rows of output SQL into a single hierarchy of entities. 68 | 69 | See the `PersonEntityMapper.cs` class in the test project for an example. 70 | 71 | # Usage 72 | 73 | ## Setup 74 | 75 | Dapper.GraphQL uses Microsoft's standard DI container, IServiceCollection, to manage all of the Dapper and GraphQL interactions. 76 | If you're developing in ASP.NET Core, you can add this to the ConfigureServices() method: 77 | 78 | ```csharp 79 | serviceCollection.AddDapperGraphQL(options => 80 | { 81 | // Add GraphQL types 82 | options.AddType(); 83 | options.AddType(); 84 | options.AddType(); 85 | options.AddType(); 86 | options.AddType(); 87 | 88 | // Add the GraphQL schema 89 | options.AddSchema(); 90 | 91 | // Add query builders for dapper 92 | options.AddQueryBuilder(); 93 | options.AddQueryBuilder(); 94 | options.AddQueryBuilder(); 95 | options.AddQueryBuilder(); 96 | }); 97 | ``` 98 | 99 | ## Queries 100 | 101 | When creating a SQL query based on a GraphQL query, you need 2 things to build the query properly: A *query builder* and *entity mapper*. 102 | 103 | ### Query builder 104 | 105 | Each entity in a system should have its own query builder, so any GraphQL queries that interact with those entities can be automatically 106 | handled, even when nested within other entities. 107 | 108 | In the above setup, the `Email` query builder looks like this: 109 | 110 | ```csharp 111 | public class EmailQueryBuilder : 112 | IQueryBuilder 113 | { 114 | public SqlQueryContext Build(SqlQueryContext query, IHaveSelectionSet context, string alias) 115 | { 116 | // Always get the ID of the email 117 | query.Select($"{alias}.Id"); 118 | // Tell Dapper where the Email class begins (at the Id we just selected) 119 | query.SplitOn("Id"); 120 | 121 | var fields = context.GetSelectedFields(); 122 | if (fields.ContainsKey("address")) 123 | { 124 | query.Select($"{alias}.Address"); 125 | } 126 | 127 | return query; 128 | } 129 | } 130 | ``` 131 | 132 | #### Arguments 133 | 134 | * The `query` represents the SQL query that's been generated so far. 135 | * The `context` is the GraphQL context - it contains the GraphQL query and what data has been requested. 136 | * The `alias` is what SQL alias the current table has. Since entities can be used more than once (multiple entities can have an `Email`, for example), it's important that our aliases are unique. 137 | 138 | #### `Build()` method 139 | 140 | Let's break down what's happening in the `Build()` method: 141 | 142 | 1. `query.Select($"{alias}.Id");` - select the Id of the entity. This is good practice, so that even if the GraphQL query didn't include the `id`, we always include it. 143 | 2. `query.SplitOn("Id");` - tell Dapper that the `Id` marks the beginning of the `Email` class. 144 | 3. `var fields = context.GetSelectedFields();` - Gets a list of fields that have been selected from GraphQL. 145 | 4. `case "address": query.Select($"{alias}.Address");` - When `address` is found in the GraphQL query, add the `Address` to the SQL SELECT clause. 146 | 147 | #### Query builder chaining 148 | 149 | Query builders are intended to chain, as our entities tend to have a hierarchical relationship. See the [PersonQueryBuilder.cs](https://github.com/landmarkhw/Dapper.GraphQL/blob/master/Dapper.GraphQL.Test/QueryBuilders/PersonQueryBuilder.cs) file in the test project for a good example of chaining. 150 | 151 | ## GraphQL integration 152 | 153 | Here's an example of a query definition in `graphql-dotnet`: 154 | 155 | ```csharp 156 | Field>( 157 | "people", 158 | description: "A list of people.", 159 | resolve: context => 160 | { 161 | // Create an alias for the 'Person' table. 162 | var alias = "person"; 163 | // Add the 'Person' table to the FROM clause in SQL 164 | var query = SqlBuilder.From($"Person {alias}"); 165 | // Build the query, using the GraphQL query and SQL table alias. 166 | query = personQueryBuilder.Build(query, context.FieldAst, alias); 167 | 168 | // Create a mapper that understands how to map the 'Person' class. 169 | var personMapper = new PersonEntityMapper(); 170 | 171 | // Open a connection to the database 172 | using (var connection = serviceProvider.GetRequiredService()) 173 | { 174 | // Execute the query with the person mapper 175 | var results = query.Execute(connection, personMapper, context.FieldAst); 176 | 177 | // `results` contains a list of people. 178 | return results; 179 | } 180 | } 181 | ); 182 | ``` 183 | 184 | # Mapping objects of the same type 185 | 186 | The test project contains an example of how to handle this scenario. See [PersonEntityMapper.cs](https://github.com/landmarkhw/Dapper.GraphQL/blob/master/Dapper.GraphQL.Test/EntityMappers/PersonEntityMapper.cs). 187 | 188 | # Examples 189 | 190 | See the Dapper.GraphQL.Test project for a full set of examples, including how *query builders* and *entity mappers* are designed. 191 | 192 | # Development & Testing 193 | 194 | To run unit tests, you must have PostgreSQL running locally on your machine. The easiest way to 195 | accomplish this is to install Docker and run PostgreSQL from the official Docker container: 196 | 197 | From a command line, run a Postgres image from docker as follows: 198 | 199 | ``` 200 | docker run --name dapper-graphql-test -e POSTGRES_PASSWORD=dapper-graphql -d -p 5432:5432 postgres 201 | ``` 202 | 203 | Then, unit tests should function as expected. 204 | 205 | # Roadmap 206 | 207 | * Fluent-style pagination 208 | --------------------------------------------------------------------------------