├── .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