- Notifications
You must be signed in to change notification settings - Fork96
GraphQL 'Star Wars' example using GraphQL for .NET, ASP.NET Core, Entity Framework Core
License
JacekKosciesza/StarWars
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
Basic - simple 'Hello GraphQL!' example based on console version fromGraphQL for .NET on GitHub,but using ASP.NET Core, Entity Framework Core and some best practices, patterns and principles.
Advanced - GraphQL queries and mutations with full 'Star Wars' database (seeGraphQL Specification by Facebook andGraphQL.js - reference implementation)
- Basic
- Simple tutorial (step/screenshot/code)
- Detailed tutorial (steps explanation)
- 3-Layers (Api, Core, Data) architecture
- DDD (Domain Driven Design) hexagonal architecture
- Dependency Inversion (deafult ASP.NET Core IoC container)
- GraphQL controller
- In Memory 'Droid' Repository
- Entity Framework 'Droid' Repository
- Automatic database creation
- Seed database data
- EF Migrations
- GraphiQL
- Unit Tests
- Visual Studio 2017 RC upgrade
- Integration Tests
- Logs
- Code Coverage
- Continous Integration
- Advanced
- Full 'Star Wars' database (Episodes, Characters, Planets, Humans etc.)
- Base/generic repository
- Visual Studio 2017 RTM upgrade
- Repositories
- GraphQL queries
- GraphQL mutations
- Docker
- PWA (Progressive Web App)
- Identity microservice
- Angular frontend
- Apollo GraphQL Client for Angular
- Service Worker
- IndexedDB
- ...
Add 'ASP.NET Core Web Application (.NET Core)' project named 'StarWars.Api'
Update project.json with correct runtime
"runtimes": {"win10-x64": { }}
namespaceStarWars.Core.Models{publicclassDroid{publicintId{get;set;}publicstringName{get;set;}}}
- Create 'DroidType' model
usingGraphQL.Types;usingStarWars.Core.Models;namespaceStarWars.Api.Models{publicclassDroidType:ObjectGraphType<Droid>{publicDroidType(){Field(x=>x.Id).Description("The Id of the Droid.");Field(x=>x.Name,nullable:true).Description("The name of the Droid.");}}}
- Create 'StarWarsQuery' model
usingGraphQL.Types;usingStarWars.Core.Models;namespaceStarWars.Api.Models{publicclassStarWarsQuery:ObjectGraphType{publicStarWarsQuery(){Field<DroidType>("hero",resolve: context=>newDroid{Id=1,Name="R2-D2"});}}}
- Create 'GraphQLQuery' model
namespaceStarWars.Api.Models{publicclassGraphQLQuery{publicstringOperationName{get;set;}publicstringNamedQuery{get;set;}publicstringQuery{get;set;}publicstringVariables{get;set;}}}
- Create 'GraphQLController'
usingGraphQL;usingGraphQL.Types;usingMicrosoft.AspNetCore.Mvc;usingStarWars.Api.Models;usingSystem.Threading.Tasks;namespaceStarWars.Api.Controllers{[Route("graphql")]publicclassGraphQLController:Controller{[HttpPost]publicasyncTask<IActionResult>Post([FromBody]GraphQLQueryquery){varschema=newSchema{Query=newStarWarsQuery()};varresult=awaitnewDocumentExecuter().ExecuteAsync(_=>{_.Schema=schema;_.Query=query.Query;}).ConfigureAwait(false);if(result.Errors?.Count>0){returnBadRequest();}returnOk(result);}}}
usingStarWars.Core.Models;usingSystem.Threading.Tasks;namespaceStarWars.Core.Data{publicinterfaceIDroidRepository{Task<Droid>Get(intid);}}
usingStarWars.Core.Data;usingSystem.Collections.Generic;usingSystem.Threading.Tasks;usingStarWars.Core.Models;usingSystem.Linq;namespaceStarWars.Data.InMemory{publicclassDroidRepository:IDroidRepository{privateList<Droid>_droids=newList<Droid>{newDroid{Id=1,Name="R2-D2"}};publicTask<Droid>Get(intid){returnTask.FromResult(_droids.FirstOrDefault(droid=>droid.Id==id));}}}
- Use 'IDroidRepository' in StarWarsQuery
usingGraphQL.Types;usingStarWars.Core.Data;namespaceStarWars.Api.Models{publicclassStarWarsQuery:ObjectGraphType{privateIDroidRepository_droidRepository{get;set;}publicStarWarsQuery(IDroidRepository_droidRepository){Field<DroidType>("hero",resolve: context=>_droidRepository.Get(1));}}}
- Update creation of StarWarsQuery in GraphQLController
// ...publicasyncTask<IActionResult>Post([FromBody]GraphQLQueryquery){varschema=newSchema{Query=newStarWarsQuery(newDroidRepository())};// ...
Test using Postman
Configure dependency injection in Startup.cs
// ...publicvoidConfigureServices(IServiceCollectionservices){services.AddMvc();services.AddTransient<StarWarsQuery>();services.AddTransient<IDroidRepository,DroidRepository>();}// ...
- Use constructor injection of StarWarsQuery in GraphQLController
// ...publicclassGraphQLController:Controller{privateStarWarsQuery_starWarsQuery{get;set;}publicGraphQLController(StarWarsQuerystarWarsQuery){_starWarsQuery=starWarsQuery;}[HttpPost]publicasyncTask<IActionResult>Post([FromBody]GraphQLQueryquery){varschema=newSchema{Query=_starWarsQuery};// ...
Add Microsoft.EntityFrameworkCore and Microsoft.EntityFrameworkCore.SqlServer Nuget packages to StarWars.Data project
Create StarWarsContext
usingMicrosoft.EntityFrameworkCore;usingStarWars.Core.Models;namespaceStarWars.Data.EntityFramework{publicclassStarWarsContext:DbContext{publicStarWarsContext(DbContextOptionsoptions):base(options){Database.EnsureCreated();}publicDbSet<Droid>Droids{get;set;}}}
- Update 'appsetting.json' with database connection
{"Logging": {"IncludeScopes":false,"LogLevel": {"Default":"Debug","System":"Information","Microsoft":"Information" } },"ConnectionStrings": {"StarWarsDatabaseConnection":"Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=StarWars;Integrated Security=SSPI;integrated security=true;MultipleActiveResultSets=True;" }}
- Create EF droid repository
usingStarWars.Core.Data;usingSystem.Threading.Tasks;usingStarWars.Core.Models;usingMicrosoft.EntityFrameworkCore;namespaceStarWars.Data.EntityFramework.Repositories{publicclassDroidRepository:IDroidRepository{privateStarWarsContext_db{get;set;}publicDroidRepository(StarWarsContextdb){_db=db;}publicTask<Droid>Get(intid){return_db.Droids.FirstOrDefaultAsync(droid=>droid.Id==id);}}}
- Create seed data as an extension to StarWarsContext
usingStarWars.Core.Models;usingSystem.Linq;namespaceStarWars.Data.EntityFramework.Seed{publicstaticclassStarWarsSeedData{publicstaticvoidEnsureSeedData(thisStarWarsContextdb){if(!db.Droids.Any()){vardroid=newDroid{Name="R2-D2"};db.Droids.Add(droid);db.SaveChanges();}}}}
- Configure dependency injection and run data seed in Startup.cs
// ...publicvoidConfigureServices(IServiceCollectionservices){services.AddMvc();services.AddTransient<StarWarsQuery>();services.AddTransient<IDroidRepository,DroidRepository>();services.AddDbContext<StarWarsContext>(options=>options.UseSqlServer(Configuration["ConnectionStrings:StarWarsDatabaseConnection"]));}publicvoidConfigure(IApplicationBuilderapp,IHostingEnvironmentenv,ILoggerFactoryloggerFactory,StarWarsContextdb){loggerFactory.AddConsole(Configuration.GetSection("Logging"));loggerFactory.AddDebug();app.UseMvc();db.EnsureSeedData();}// ...
Add 'Microsoft.EntityFrameworkCore.Design' NuGet package to 'StarWars.Data' project
Add 'Microsoft.EntityFrameworkCore.Tools.DotNet' NuGet package to 'StarWars.Data' project
Add tools section in project.json (StarWars.Data)
"tools": {"Microsoft.EntityFrameworkCore.Tools.DotNet":"1.1.0-preview4-final"}
- Add official workaround for problems with targeting class library (Modify your class library to be a startup application)
- Add main entry point
namespaceStarWars.Data.EntityFramework.Workaround{// WORKAROUND: https://docs.efproject.net/en/latest/miscellaneous/cli/dotnet.html#targeting-class-library-projects-is-not-supportedpublicstaticclassProgram{publicstaticvoidMain(){}}}
- Add build option in project.json
"buildOptions": {"emitEntryPoint":true}
- Run migrations command from the console
dotnet ef migrations add Inital -o .\EntityFramework\Migrations
Add NPM configuration file 'package.json' to StarWars.Api project
Add GraphiQL dependencies and webpack bundle task
{"version":"1.0.0","name":"starwars-graphiql","private":true,"scripts": {"start":"webpack --progress" },"dependencies": {"graphiql":"^0.7.8","graphql":"^0.7.0","isomorphic-fetch":"^2.1.1","react":"^15.3.1","react-dom":"^15.3.1" },"devDependencies": {"babel":"^5.6.14","babel-loader":"^5.3.2","css-loader":"^0.24.0","extract-text-webpack-plugin":"^1.0.1","postcss-loader":"^0.10.1","style-loader":"^0.13.1","webpack":"^1.13.0" }}
- Add webpack configuration 'webpack.config.js'
varwebpack=require('webpack');varExtractTextPlugin=require('extract-text-webpack-plugin');varoutput='./wwwroot';module.exports={entry:{'bundle':'./Scripts/app.js'},output:{path:output,filename:'[name].js'},resolve:{extensions:['','.js','.json']},module:{loaders:[{test:/\.js/,loader:'babel',exclude:/node_modules/},{test:/\.css$/,loader:ExtractTextPlugin.extract('style-loader','css-loader!postcss-loader')}]},plugins:[newExtractTextPlugin('style.css',{allChunks:true})]};
"-vs-binding": {"AfterBuild": ["start" ] }
- Add 'Get' action to GraphQL controller and GraphiQL view (~/Views/GraphQL/index.cshtml)
// ...publicclassGraphQLController:Controller{// ...[HttpGet]publicIActionResultIndex(){returnView();}// ...}
<!DOCTYPE html><html><head><metacharset="utf-8"/><metaname="viewport"content="width=device-width"/><title>GraphiQL</title><linkrel="stylesheet"href="~/style.css"/></head><body><divid="app"></div><scriptsrc="~/bundle.js"type="text/javascript"></script></body></html>
Add GraphiQL scripts and styles (app.js and app.css to ~/GraphiQL)
- app.js
importReactfrom'react';importReactDOMfrom'react-dom';importGraphiQLfrom'graphiql';importfetchfrom'isomorphic-fetch';import'graphiql/graphiql.css';import'./app.css';functiongraphQLFetcher(graphQLParams){returnfetch(window.location.origin+'/graphql',{method:'post',headers:{'Content-Type':'application/json'},body:JSON.stringify(graphQLParams)}).then(response=>response.json());}ReactDOM.render(<GraphiQLfetcher={graphQLFetcher}/>,document.getElementById('app'));
- app.css
html,body {height:100%;margin:0;overflow: hidden;width:100%;}#app {height:100vh;}
Add static files support
// ...publicvoidConfigure(IApplicationBuilderapp,IHostingEnvironmentenv,ILoggerFactoryloggerFactory,StarWarsContextdb){loggerFactory.AddConsole(Configuration.GetSection("Logging"));loggerFactory.AddDebug();app.UseStaticFiles();app.UseMvc();db.EnsureSeedData();}// ...
Build project and check if bundles were created by webpack under ~/wwwroot
Create 'Class Library (.NET Core)' type 'StarWars.Tests.Unit' project
Install 'xunit' NuGet package in StarWars.Tests.Unit project
Install 'dotnet-test-xunit' NuGet package in StarWars.Tests.Unit project
Make changes to project.json
- Set 'testRunner'
- Reference 'StarWars.Data' project
- Set 'runtimes'
{"version":"1.0.0-*","testRunner":"xunit","dependencies": {"dotnet-test-xunit":"2.2.0-preview2-build1029","Microsoft.NETCore.App":"1.1.0","xunit":"2.1.0","StarWars.Data": {"target":"project" } },"frameworks": {"netcoreapp1.1": {"imports": ["dotnet5.6","portable-net45+win8" ] } },"runtimes": {"win10-x64": {} }}
- Create first test for in memory droid repository
usingStarWars.Data.InMemory;usingXunit;namespaceStarWars.Tests.Unit.Data.InMemory{publicclassDroidRepositoryShould{privatereadonlyDroidRepository_droidRepository;publicDroidRepositoryShould(){// Given_droidRepository=newDroidRepository();}[Fact]publicasyncvoidReturnR2D2DroidGivenIdOf1(){// Whenvardroid=await_droidRepository.Get(1);// ThenAssert.NotNull(droid);Assert.Equal("WRONG_NAME",droid.Name);}}}
Build and make sure that test is discovered by 'Test Explorer'
Run test - it should fail (we want to make sure that we are testing the right thing)
Fix test
// ...[Fact]publicasyncvoidReturnR2D2DroidGivenIdOf1(){// Whenvardroid=await_droidRepository.Get(1);// ThenAssert.NotNull(droid);Assert.Equal("R2-D2",droid.Name);}// ...
Install 'Microsoft.EntityFrameworkCore.InMemory' NuGet package
Add reference to 'StarWars.Core' in project.json
{"dependencies": {"StarWars.Core": {"target":"project" } }}
- Create EF droid repository unit test
usingMicrosoft.EntityFrameworkCore;usingStarWars.Core.Models;usingStarWars.Data.EntityFramework;usingStarWars.Data.EntityFramework.Repositories;usingXunit;namespaceStarWars.Tests.Unit.Data.EntityFramework.Repositories{publicclassDroidRepositoryShould{privatereadonlyDroidRepository_droidRepository;publicDroidRepositoryShould(){// Given// https://docs.microsoft.com/en-us/ef/core/miscellaneous/testing/in-memoryvaroptions=newDbContextOptionsBuilder<StarWarsContext>().UseInMemoryDatabase(databaseName:"StarWars").Options;using(varcontext=newStarWarsContext(options)){context.Droids.Add(newDroid{Id=1,Name="R2-D2"});context.SaveChanges();}varstarWarsContext=newStarWarsContext(options);_droidRepository=newDroidRepository(starWarsContext);}[Fact]publicasyncvoidReturnR2D2DroidGivenIdOf1(){// Whenvardroid=await_droidRepository.Get(1);// ThenAssert.NotNull(droid);Assert.Equal("R2-D2",droid.Name);}}}
- Create GraphQLController unit test
- First refactor controller to be more testable by using constructor injection
usingGraphQL;usingGraphQL.Types;usingMicrosoft.AspNetCore.Mvc;usingStarWars.Api.Models;usingSystem.Threading.Tasks;namespaceStarWars.Api.Controllers{[Route("graphql")]publicclassGraphQLController:Controller{privateIDocumentExecuter_documentExecuter{get;set;}privateISchema_schema{get;set;}publicGraphQLController(IDocumentExecuterdocumentExecuter,ISchemaschema){_documentExecuter=documentExecuter;_schema=schema;}[HttpGet]publicIActionResultIndex(){returnView();}[HttpPost]publicasyncTask<IActionResult>Post([FromBody]GraphQLQueryquery){varexecutionOptions=newExecutionOptions{Schema=_schema,Query=query.Query};varresult=await_documentExecuter.ExecuteAsync(executionOptions).ConfigureAwait(false);if(result.Errors?.Count>0){returnBadRequest(result.Errors);}returnOk(result);}}}
- Configure dependency injection in 'Startup.cs'
// ...publicvoidConfigureServices(IServiceCollectionservices){// ...services.AddTransient<IDocumentExecuter,DocumentExecuter>();varsp=services.BuildServiceProvider();services.AddTransient<ISchema>(_=>newSchema{Query=sp.GetService<StarWarsQuery>()});}// ...
- Create test for 'Index' and 'Post' actions
usingGraphQL;usingGraphQL.Types;usingMicrosoft.AspNetCore.Mvc;usingMoq;usingStarWars.Api.Controllers;usingStarWars.Api.Models;usingSystem.Threading.Tasks;usingXunit;namespaceStarWars.Tests.Unit.Api.Controllers{publicclassGraphQLControllerShould{privateGraphQLController_graphqlController{get;set;}publicGraphQLControllerShould(){// GivenvardocumentExecutor=newMock<IDocumentExecuter>();documentExecutor.Setup(x=>x.ExecuteAsync(It.IsAny<ExecutionOptions>())).Returns(Task.FromResult(newExecutionResult()));varschema=newMock<ISchema>();_graphqlController=newGraphQLController(documentExecutor.Object,schema.Object);}[Fact]publicvoidReturnNotNullViewResult(){// Whenvarresult=_graphqlController.Index()asViewResult;// ThenAssert.NotNull(result);Assert.IsType<ViewResult>(result);}[Fact]publicasyncvoidReturnNotNullExecutionResult(){// Givenvarquery=newGraphQLQuery{Query=@"{ ""query"": ""query { hero { id name } }"""};// Whenvarresult=await_graphqlController.Post(query);// ThenAssert.NotNull(result);varokObjectResult=Assert.IsType<OkObjectResult>(result);varexecutionResult=okObjectResult.Value;Assert.NotNull(executionResult);}}}
Open solution in VS 2017 and let the upgrade tool do the job
Upgrade of 'StarWars.Tests.Unit' failed, so I had to remove all project dependencies and reload it
{"dependencies": {// remove this:"StarWars.Data": {"target":"project" },"StarWars.Core": {"target":"project" }// ... }}
Replace old test txplorer runner for the xUnit.net framework (dotnet-test-xunit) with new one (xunit.runner.visualstudio)
Install (xunit.runner.visualstudio) dependency (Microsoft.DotNet.InternalAbstractions)
Create 'xUnit Test Project (.NET Core)' type 'StarWars.Tests.Integration' project
Change target framework from 'netcoreapp1.0' to 'netcoreapp1.1'
<ProjectSdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>netcoreapp1.1</TargetFramework> </PropertyGroup><!--...--></Project>
Use EF in memory database for 'Test' evironment
// ...privateIHostingEnvironmentEnv{get;set;}publicclassStartup{// ...Env=env;}publicvoidConfigureServices(IServiceCollectionservices){// ...if(Env.IsEnvironment("Test")){services.AddDbContext<StarWarsContext>(options=>options.UseInMemoryDatabase(databaseName:"StarWars"));}else{services.AddDbContext<StarWarsContext>(options=>options.UseSqlServer(Configuration["ConnectionStrings:StarWarsDatabaseConnection"]));}// ...}// ...
Create integration test for GraphQL query (POST)
usingMicrosoft.AspNetCore.Hosting;usingMicrosoft.AspNetCore.TestHost;usingStarWars.Api;usingSystem.Net.Http;usingSystem.Text;usingXunit;namespaceStarWars.Tests.Integration.Api.Controllers{publicclassGraphQLControllerShould{privatereadonlyTestServer_server;privatereadonlyHttpClient_client;publicGraphQLControllerShould(){_server=newTestServer(newWebHostBuilder().UseEnvironment("Test").UseStartup<Startup>());_client=_server.CreateClient();}[Fact]publicasyncvoidReturnR2D2Droid(){// Givenvarquery=@"{ ""query"": ""query { hero { id name } }"" }";varcontent=newStringContent(query,Encoding.UTF8,"application/json");// Whenvarresponse=await_client.PostAsync("/graphql",content);// Thenresponse.EnsureSuccessStatusCode();varresponseString=awaitresponse.Content.ReadAsStringAsync();Assert.Contains("R2-D2",responseString);}}}
- Make sure that logger is configured in Startup.cs
publicvoidConfigure(IApplicationBuilderapp,IHostingEnvironmentenv,ILoggerFactoryloggerFactory,StarWarsContextdb){loggerFactory.AddConsole(Configuration.GetSection("Logging"));loggerFactory.AddDebug();// ...}
- Override ToString method of GraphQLQuery class
publicoverridestringToString(){varbuilder=newStringBuilder();builder.AppendLine();if(!string.IsNullOrWhiteSpace(OperationName)){builder.AppendLine($"OperationName ={OperationName}");}if(!string.IsNullOrWhiteSpace(NamedQuery)){builder.AppendLine($"NamedQuery ={NamedQuery}");}if(!string.IsNullOrWhiteSpace(Query)){builder.AppendLine($"Query ={Query}");}if(!string.IsNullOrWhiteSpace(Variables)){builder.AppendLine($"Variables ={Variables}");}returnbuilder.ToString();}
- Add logger to GraphQLController
publicclassGraphQLController:Controller{// ...privatereadonlyILogger_logger;publicGraphQLController(IDocumentExecuterdocumentExecuter,ISchemaschema,ILogger<GraphQLController>logger){// ..._logger=logger;}[HttpGet]publicIActionResultIndex(){_logger.LogInformation("Got request for GraphiQL. Sending GUI back");returnView();}[HttpPost]publicasyncTask<IActionResult>Post([FromBody]GraphQLQueryquery){// ...if(result.Errors?.Count>0){_logger.LogError("GraphQL errors: {0}",result.Errors);returnBadRequest(result);}_logger.LogDebug("GraphQL execution result: {result}",JsonConvert.SerializeObject(result.Data));returnOk(result);}}
- Add logger to DroidRepository
namespaceStarWars.Data.EntityFramework.Repositories{publicclassDroidRepository:IDroidRepository{privateStarWarsContext_db{get;set;}privatereadonlyILogger_logger;publicDroidRepository(StarWarsContextdb,ILogger<DroidRepository>logger){_db=db;_logger=logger;}publicTask<Droid>Get(intid){_logger.LogInformation("Get droid with id = {id}",id);return_db.Droids.FirstOrDefaultAsync(droid=>droid.Id==id);}}}
- Add logger to StarWarsContext
namespaceStarWars.Data.EntityFramework{publicclassStarWarsContext:DbContext{publicreadonlyILogger_logger;publicStarWarsContext(DbContextOptionsoptions,ILogger<StarWarsContext>logger):base(options){_logger=logger;// ...}}}
- Add logger to StarWarsSeedData
namespaceStarWars.Data.EntityFramework.Seed{publicstaticclassStarWarsSeedData{publicstaticvoidEnsureSeedData(thisStarWarsContextdb){db._logger.LogInformation("Seeding database");if(!db.Droids.Any()){db._logger.LogInformation("Seeding droids");// ...}}}}
- Fix controller unit test
publicclassGraphQLControllerShould{publicGraphQLControllerShould(){// ...varlogger=newMock<ILogger<GraphQLController>>();_graphqlController=newGraphQLController(documentExecutor.Object,schema.Object,logger.Object);}// ...}
- Fix repository unit test
usingMicrosoft.EntityFrameworkCore;usingMicrosoft.Extensions.Logging;usingMoq;usingStarWars.Core.Models;usingStarWars.Data.EntityFramework;usingStarWars.Data.EntityFramework.Repositories;usingXunit;namespaceStarWars.Tests.Unit.Data.EntityFramework.Repositories{publicclassDroidRepositoryShould{publicDroidRepositoryShould(){vardbLogger=newMock<ILogger<StarWarsContext>>();// ...using(varcontext=newStarWarsContext(options,dbLogger.Object)){// ...}// ...varrepoLogger=newMock<ILogger<DroidRepository>>();_droidRepository=newDroidRepository(starWarsContext,repoLogger.Object);}}}
Add path to OpenCover tools to 'Path' environment variable. In my case it was:
C:\Users\Jacek_Kosciesza\.nuget\packages\opencover\4.6.519\tools\
- Set 'Full' debug type in all projects (StarWars.Api.csproj, StarWars.Core.csproj, StarWars.Data.csproj). This is needed to produce *.pdb files which are understandable by OpenCover.
<ProjectSdk="Microsoft.NET.Sdk.Web"> <PropertyGroup><!--...--> <DebugType>Full</DebugType> </PropertyGroup><!--...--></Project>
- Run OpenCover in the console
OpenCover.Console.exe -target:"dotnet.exe" -targetargs:"test -f netcoreapp1.1 -c Release Tests/StarWars.Tests.Unit/StarWars.Tests.Unit.csproj" -hideskipped:File -output:coverage/unit/coverage.xml -oldStyle -filter:"+[StarWars*]* -[StarWars.Tests*]* -[StarWars.Api]*Program -[StarWars.Api]*Startup -[StarWars.Data]*EntityFramework.Workaround.Program -[StarWars.Data]*EntityFramework.Migrations* -[StarWars.Data]*EntityFramework.Seed*" -searchdirs:"Tests/StarWars.Tests.Unit/bin/Release/netcoreapp1.1" -register:user
mkdir coverage\unitOpenCover.Console.exe -target:"dotnet.exe" -targetargs:"test -f netcoreapp1.1 -c Release Tests/StarWars.Tests.Unit/StarWars.Tests.Unit.csproj" -hideskipped:File -output:coverage/unit/coverage.xml -oldStyle -filter:"+[StarWars*]* -[StarWars.Tests*]* -[StarWars.Api]*Program -[StarWars.Api]*Startup -[StarWars.Data]*EntityFramework.Workaround.Program -[StarWars.Data]*EntityFramework.Migrations* -[StarWars.Data]*EntityFramework.Seed*" -searchdirs:"Tests/StarWars.Tests.Unit/bin/Release/netcoreapp1.1" -register:userReportGenerator.exe -reports:coverage/unit/coverage.xml -targetdir:coverage/unit -verbosity:Errorstart .\coverage\unit\index.htm
Create new build definition "ASP.NET Core Preview". Select GitHub, Hosted VS2017 default agent queue and continous integration.
At the moment hosted agents don't support *.csproj based .NET Core projects, so we have to wait for a while, see this issue:Support for .NET Core .csproj files? #3311Setup projects in Test build step
**/Tests/StarWars.Tests.Unit/StarWars.Tests.Unit.csproj;**/Tests/StarWars.Tests.Integration/StarWars.Tests.Integration.csproj
Queue build. Make sure it succeeded and executed unit and integration tests.
Enable build badge (after save you will see link to build status image).
Full 'Star Wars' database (seeFacebook GraphQL andGraphQL.js)
- Create models
namespaceStarWars.Core.Models{publicclassEpisode{publicintId{get;set;}publicstringTitle{get;set;}publicvirtualICollection<CharacterEpisode>CharacterEpisodes{get;set;}}}
namespaceStarWars.Core.Models{publicclassPlanet{publicintId{get;set;}publicstringName{get;set;}publicICollection<Human>Humans{get;set;}}}
namespaceStarWars.Core.Models{publicclassCharacter{publicintId{get;set;}publicstringName{get;set;}publicvirtualICollection<CharacterEpisode>CharacterEpisodes{get;set;}publicvirtualICollection<CharacterFriend>CharacterFriends{get;set;}publicvirtualICollection<CharacterFriend>FriendCharacters{get;set;}}}
namespaceStarWars.Core.Models{publicclassCharacterEpisode{publicintCharacterId{get;set;}publicCharacterCharacter{get;set;}publicintEpisodeId{get;set;}publicEpisodeEpisode{get;set;}}}
namespaceStarWars.Core.Models{publicclassCharacterFriend{publicintCharacterId{get;set;}publicCharacterCharacter{get;set;}publicintFriendId{get;set;}publicCharacterFriend{get;set;}}}
namespaceStarWars.Core.Models{publicclassDroid:Character{publicstringPrimaryFunction{get;set;}}}
namespaceStarWars.Core.Models{publicclassHuman:Character{publicPlanetHomePlanet{get;set;}}}
- Update StarWarsContext
namespaceStarWars.Data.EntityFramework{publicclassStarWarsContext:DbContext{// ...protectedoverridevoidOnModelCreating(ModelBuildermodelBuilder){// https://docs.microsoft.com/en-us/ef/core/modeling/relationships// http://stackoverflow.com/questions/38520695/multiple-relationships-to-the-same-table-in-ef7core// episodesmodelBuilder.Entity<Episode>().HasKey(c=>c.Id);modelBuilder.Entity<Episode>().Property(e=>e.Id).ValueGeneratedNever();// planetsmodelBuilder.Entity<Planet>().HasKey(c=>c.Id);modelBuilder.Entity<Planet>().Property(e=>e.Id).ValueGeneratedNever();// charactersmodelBuilder.Entity<Character>().HasKey(c=>c.Id);modelBuilder.Entity<Character>().Property(e=>e.Id).ValueGeneratedNever();// characters-friendsmodelBuilder.Entity<CharacterFriend>().HasKey(t=>new{t.CharacterId,t.FriendId});modelBuilder.Entity<CharacterFriend>().HasOne(cf=>cf.Character).WithMany(c=>c.CharacterFriends).HasForeignKey(cf=>cf.CharacterId).OnDelete(DeleteBehavior.Restrict);modelBuilder.Entity<CharacterFriend>().HasOne(cf=>cf.Friend).WithMany(t=>t.FriendCharacters).HasForeignKey(cf=>cf.FriendId).OnDelete(DeleteBehavior.Restrict);// characters-episodesmodelBuilder.Entity<CharacterEpisode>().HasKey(t=>new{t.CharacterId,t.EpisodeId});modelBuilder.Entity<CharacterEpisode>().HasOne(cf=>cf.Character).WithMany(c=>c.CharacterEpisodes).HasForeignKey(cf=>cf.CharacterId).OnDelete(DeleteBehavior.Restrict);modelBuilder.Entity<CharacterEpisode>().HasOne(cf=>cf.Episode).WithMany(t=>t.CharacterEpisodes).HasForeignKey(cf=>cf.EpisodeId).OnDelete(DeleteBehavior.Restrict);// humansmodelBuilder.Entity<Human>().HasOne(h=>h.HomePlanet).WithMany(p=>p.Humans);}publicvirtualDbSet<Episode>Episodes{get;set;}publicvirtualDbSet<Planet>Planets{get;set;}publicvirtualDbSet<Character>Characters{get;set;}publicvirtualDbSet<CharacterFriend>CharacterFriends{get;set;}publicvirtualDbSet<CharacterEpisode>CharacterEpisodes{get;set;}publicvirtualDbSet<Droid>Droids{get;set;}publicvirtualDbSet<Human>Humans{get;set;}}}
- Update database seed data
namespaceStarWars.Data.EntityFramework.Seed{publicstaticclassStarWarsSeedData{publicstaticvoidEnsureSeedData(thisStarWarsContextdb){db._logger.LogInformation("Seeding database");// episodesvarnewhope=newEpisode{Id=4,Title="NEWHOPE"};varempire=newEpisode{Id=5,Title="EMPIRE"};varjedi=newEpisode{Id=6,Title="JEDI"};varepisodes=newList<Episode>{newhope,empire,jedi,};if(!db.Episodes.Any()){db._logger.LogInformation("Seeding episodes");db.Episodes.AddRange(episodes);db.SaveChanges();}// planetsvartatooine=newPlanet{Id=1,Name="Tatooine"};varalderaan=newPlanet{Id=2,Name="Alderaan"};varplanets=newList<Planet>{tatooine,alderaan};if(!db.Planets.Any()){db._logger.LogInformation("Seeding planets");db.Planets.AddRange(planets);db.SaveChanges();}// humansvarluke=newHuman{Id=1000,Name="Luke Skywalker",CharacterEpisodes=newList<CharacterEpisode>{newCharacterEpisode{Episode=newhope},newCharacterEpisode{Episode=empire},newCharacterEpisode{Episode=jedi}},HomePlanet=tatooine};varvader=newHuman{Id=1001,Name="Darth Vader",CharacterEpisodes=newList<CharacterEpisode>{newCharacterEpisode{Episode=newhope},newCharacterEpisode{Episode=empire},newCharacterEpisode{Episode=jedi}},HomePlanet=tatooine};varhan=newHuman{Id=1002,Name="Han Solo",CharacterEpisodes=newList<CharacterEpisode>{newCharacterEpisode{Episode=newhope},newCharacterEpisode{Episode=empire},newCharacterEpisode{Episode=jedi}},HomePlanet=tatooine};varleia=newHuman{Id=1003,Name="Leia Organa",CharacterEpisodes=newList<CharacterEpisode>{newCharacterEpisode{Episode=newhope},newCharacterEpisode{Episode=empire},newCharacterEpisode{Episode=jedi}},HomePlanet=alderaan};vartarkin=newHuman{Id=1004,Name="Wilhuff Tarkin",CharacterEpisodes=newList<CharacterEpisode>{newCharacterEpisode{Episode=newhope}},};varhumans=newList<Human>{luke,vader,han,leia,tarkin};if(!db.Humans.Any()){db._logger.LogInformation("Seeding humans");db.Humans.AddRange(humans);db.SaveChanges();}// droidsvarthreepio=newDroid{Id=2000,Name="C-3PO",CharacterEpisodes=newList<CharacterEpisode>{newCharacterEpisode{Episode=newhope},newCharacterEpisode{Episode=empire},newCharacterEpisode{Episode=jedi}},PrimaryFunction="Protocol"};varartoo=newDroid{Id=2001,Name="R2-D2",CharacterEpisodes=newList<CharacterEpisode>{newCharacterEpisode{Episode=newhope},newCharacterEpisode{Episode=empire},newCharacterEpisode{Episode=jedi}},PrimaryFunction="Astromech"};vardroids=newList<Droid>{threepio,artoo};if(!db.Droids.Any()){db._logger.LogInformation("Seeding droids");db.Droids.AddRange(droids);db.SaveChanges();}// update character's friendsluke.CharacterFriends=newList<CharacterFriend>{newCharacterFriend{Friend=han},newCharacterFriend{Friend=leia},newCharacterFriend{Friend=threepio},newCharacterFriend{Friend=artoo}};vader.CharacterFriends=newList<CharacterFriend>{newCharacterFriend{Friend=tarkin}};han.CharacterFriends=newList<CharacterFriend>{newCharacterFriend{Friend=luke},newCharacterFriend{Friend=leia},newCharacterFriend{Friend=artoo}};leia.CharacterFriends=newList<CharacterFriend>{newCharacterFriend{Friend=luke},newCharacterFriend{Friend=han},newCharacterFriend{Friend=threepio},newCharacterFriend{Friend=artoo}};tarkin.CharacterFriends=newList<CharacterFriend>{newCharacterFriend{Friend=vader}};threepio.CharacterFriends=newList<CharacterFriend>{newCharacterFriend{Friend=luke},newCharacterFriend{Friend=han},newCharacterFriend{Friend=leia},newCharacterFriend{Friend=artoo}};artoo.CharacterFriends=newList<CharacterFriend>{newCharacterFriend{Friend=luke},newCharacterFriend{Friend=han},newCharacterFriend{Friend=leia}};varcharacters=newList<Character>{luke,vader,han,leia,tarkin,threepio,artoo};if(!db.CharacterFriends.Any()){db._logger.LogInformation("Seeding character's friends");db.Characters.UpdateRange(characters);db.SaveChanges();}}}}
Set 'StarWars.Data' as a StartUp project
Set 'StarWars.Api' as a StartUp project
Create integration test checking EF configuration and seeded data
namespaceStarWars.Tests.Integration.Data.EntityFramework{publicclassStarWarsContextShould{[Fact]publicasyncvoidReturnR2D2Droid(){// Givenusing(vardb=newStarWarsContext()){// Whenvarr2d2=awaitdb.Droids.Include("CharacterEpisodes.Episode").Include("CharacterFriends.Friend").FirstOrDefaultAsync(d=>d.Id==2001);// ThenAssert.NotNull(r2d2);Assert.Equal("R2-D2",r2d2.Name);Assert.Equal("Astromech",r2d2.PrimaryFunction);varepisodes=r2d2.CharacterEpisodes.Select(e=>e.Episode.Title);Assert.Equal(newstring[]{"NEWHOPE","EMPIRE","JEDI"},episodes);varfriends=r2d2.CharacterFriends.Select(e=>e.Friend.Name);Assert.Equal(newstring[]{"Luke Skywalker","Han Solo","Leia Organa"},friends);}}}}
namespaceStarWars.Api.Models{publicclassStarWarsQuery:ObjectGraphType{// ...publicStarWarsQuery(IDroidRepository_droidRepository){Field<DroidType>("hero",resolve: context=>_droidRepository.Get(2001));}}}
- Create generic entity interface
namespaceStarWars.Core.Data{publicinterfaceIEntity<TKey>{TKeyId{get;set;}}}
- Update models to inherit from IEntity interface (integer based id)
namespaceStarWars.Core.Models{publicclassCharacter:IEntity<int>{// ...}}
namespaceStarWars.Core.Models{publicclassEpisode:IEntity<int>{// ...}}
namespaceStarWars.Core.Models{publicclassPlanet:IEntity<int>{// ...}}
- Create base/generic repository interface
namespaceStarWars.Core.Data{publicinterfaceIBaseRepository<TEntity,inTKey>whereTEntity:class{Task<List<TEntity>>GetAll();Task<TEntity>Get(TKeyid);TEntityAdd(TEntityentity);voidAddRange(IEnumerable<TEntity>entities);voidDelete(TKeyid);voidUpdate(TEntityentity);Task<bool>SaveChangesAsync();}}
- Create Entity Framework base/generic repository
namespaceStarWars.Data.EntityFramework.Repositories{publicabstractclassBaseRepository<TEntity,TKey>:IBaseRepository<TEntity,TKey>whereTEntity:class,IEntity<TKey>,new(){protectedDbContext_db;protectedreadonlyILogger_logger;protectedBaseRepository(){}protectedBaseRepository(DbContextdb,ILoggerlogger){_db=db;_logger=logger;}publicvirtualTask<List<TEntity>>GetAll(){return_db.Set<TEntity>().ToListAsync();}publicvirtualTask<TEntity>Get(TKeyid){_logger.LogInformation("Get {type} with id = {id}",typeof(TEntity).Name,id);return_db.Set<TEntity>().SingleOrDefaultAsync(c=>c.Id.Equals(id));}publicvirtualTEntityAdd(TEntityentity){_db.Set<TEntity>().Add(entity);returnentity;}publicvoidAddRange(IEnumerable<TEntity>entities){_db.Set<TEntity>().AddRange(entities);}publicvirtualvoidDelete(TKeyid){varentity=newTEntity{Id=id};_db.Set<TEntity>().Attach(entity);_db.Set<TEntity>().Remove(entity);}publicvirtualasyncTask<bool>SaveChangesAsync(){return(await_db.SaveChangesAsync())>0;}publicvirtualvoidUpdate(TEntityentity){_db.Set<TEntity>().Attach(entity);_db.Entry(entity).State=EntityState.Modified;}}}
- Refactor EF Droid repository
namespaceStarWars.Core.Data{publicinterfaceIDroidRepository:IBaseRepository<Droid,int>{}}
namespaceStarWars.Data.EntityFramework.Repositories{publicclassDroidRepository:BaseRepository<Droid,int>,IDroidRepository{publicDroidRepository(){}publicDroidRepository(StarWarsContextdb,ILogger<DroidRepository>logger):base(db,logger){}}}
- Refactor in-memeory Droid repository
namespaceStarWars.Data.InMemory{publicclassDroidRepository:IDroidRepository{privatereadonlyILogger_logger;publicDroidRepository(){}publicDroidRepository(ILogger<DroidRepository>logger){_logger=logger;}privateList<Droid>_droids=newList<Droid>{newDroid{Id=1,Name="R2-D2"}};publicTask<Droid>Get(intid){_logger.LogInformation("Get droid with id = {id}",id);returnTask.FromResult(_droids.FirstOrDefault(droid=>droid.Id==id));}// ...// rest of the methods are not implemented// for now they are just throwing NotImplementedException}}
- Make sure tests and api stil works
Update all NuGet packages for the solution (especially .NET Core v1.1.1)
Use 'Package Manger Console' to fix problems with upgrading 'Microsoft.NETCore.App' from v1.1.0 to v.1.1.1 (for some reason Consolidate option does not work). Do upgrade for all projects.
Install-Package Microsoft.NETCore.App
- Fix 'DroidType' unit test (capitalization of field names)
namespaceStarWars.Tests.Unit.Api.Models{publicclassDroidTypeShould{[Fact]publicvoidHaveIdAndNameFields(){// WhenvardroidType=newDroidType();// ThenAssert.NotNull(droidType);Assert.True(droidType.HasField("Id"));Assert.True(droidType.HasField("Name"));}}}
- Create rest of the repositories (Character, Episode, Human, Planet)
namespaceStarWars.Core.Data{publicinterfaceIHumanRepository:IBaseRepository<Human,int>{}}
namespaceStarWars.Data.EntityFramework.Repositories{publicclassHumanRepository:BaseRepository<Human,int>,IHumanRepository{publicHumanRepository(){}publicHumanRepository(StarWarsContextdb,ILogger<HumanRepository>logger):base(db,logger){}}}
- Update base repository with 'include' versions
namespaceStarWars.Core.Data{publicinterfaceIBaseRepository<TEntity,inTKey>whereTEntity:class{// ...Task<List<TEntity>>GetAll(stringinclude);Task<List<TEntity>>GetAll(IEnumerable<string>includes);// ...Task<TEntity>Get(TKeyid,stringinclude);Task<TEntity>Get(TKeyid,IEnumerable<string>includes);// ...}}
namespaceStarWars.Data.EntityFramework.Repositories{publicabstractclassBaseRepository<TEntity,TKey>:IBaseRepository<TEntity,TKey>whereTEntity:class,IEntity<TKey>,new(){// ...publicTask<List<TEntity>>GetAll(stringinclude){_logger.LogInformation("Get all {type}s (including {include})",typeof(TEntity).Name,include);return_db.Set<TEntity>().Include(include).ToListAsync();}publicTask<List<TEntity>>GetAll(IEnumerable<string>includes){_logger.LogInformation("Get all {type}s (including [{includes}])",typeof(TEntity).Name,string.Join(",",includes));varquery=_db.Set<TEntity>().AsQueryable();query=includes.Aggregate(query,(current,include)=>current.Include(include));returnquery.ToListAsync();}// ...publicTask<TEntity>Get(TKeyid,stringinclude){_logger.LogInformation("Get {type} with id = {id} (including {include})",typeof(TEntity).Name,id,include);return_db.Set<TEntity>().Include(include).SingleOrDefaultAsync(c=>c.Id.Equals(id));}publicTask<TEntity>Get(TKeyid,IEnumerable<string>includes){_logger.LogInformation("Get {type} with id = {id} (including [{include}])",typeof(TEntity).Name,id,string.Join(",",includes));varquery=_db.Set<TEntity>().AsQueryable();query=includes.Aggregate(query,(current,include)=>current.Include(include));returnquery.SingleOrDefaultAsync(c=>c.Id.Equals(id));}// ...}}
- Create repositories CRUD unit tests
namespaceStarWars.Tests.Unit.Data.EntityFramework.Repositories{publicclassHumanRepositoryShould{privatereadonlyHumanRepository_humanRepository;privateDbContextOptions<StarWarsContext>_options;privateMock<ILogger<StarWarsContext>>_dbLogger;publicHumanRepositoryShould(){// Given_dbLogger=newMock<ILogger<StarWarsContext>>();_options=newDbContextOptionsBuilder<StarWarsContext>().UseInMemoryDatabase(databaseName:"StarWars_HumanRepositoryShould").Options;using(varcontext=newStarWarsContext(_options,_dbLogger.Object)){context.EnsureSeedData();}varstarWarsContext=newStarWarsContext(_options,_dbLogger.Object);varrepoLogger=newMock<ILogger<HumanRepository>>();_humanRepository=newHumanRepository(starWarsContext,repoLogger.Object);}[Fact]publicasyncvoidReturnLukeGivenIdOf1000(){// Whenvarluke=await_humanRepository.Get(1000);// ThenAssert.NotNull(luke);Assert.Equal("Luke Skywalker",luke.Name);}[Fact]publicasyncvoidReturnLukeFriendsAndEpisodes(){// Whenvarcharacter=await_humanRepository.Get(1000,includes:new[]{"CharacterEpisodes.Episode","CharacterFriends.Friend"});// ThenAssert.NotNull(character);Assert.NotNull(character.CharacterEpisodes);varepisodes=character.CharacterEpisodes.Select(e=>e.Episode.Title);Assert.Equal(new[]{"NEWHOPE","EMPIRE","JEDI"},episodes);Assert.NotNull(character.CharacterFriends);varfriends=character.CharacterFriends.Select(e=>e.Friend.Name);Assert.Equal(new[]{"Han Solo","Leia Organa","C-3PO","R2-D2"},friends);}[Fact]publicasyncvoidReturnLukesHomePlanet(){// Whenvarluke=await_humanRepository.Get(1000,include:"HomePlanet");// ThenAssert.NotNull(luke);Assert.NotNull(luke.HomePlanet);Assert.Equal("Tatooine",luke.HomePlanet.Name);}[Fact]publicasyncvoidAddNewHuman(){// Givenvarhuman10101=newHuman{Id=10101,Name="Human10101"};// When_humanRepository.Add(human10101);varsaved=await_humanRepository.SaveChangesAsync();// ThenAssert.True(saved);using(vardb=newStarWarsContext(_options,_dbLogger.Object)){varhuman=awaitdb.Humans.FindAsync(10101);Assert.NotNull(human);Assert.Equal(10101,human.Id);Assert.Equal("Human10101",human.Name);// Cleanupdb.Humans.Remove(human);awaitdb.SaveChangesAsync();}}[Fact]publicasyncvoidUpdateExistingHuman(){// Givenvarvader=await_humanRepository.Get(1001);vader.Name="Human1001";// When_humanRepository.Update(vader);varsaved=await_humanRepository.SaveChangesAsync();// ThenAssert.True(saved);using(vardb=newStarWarsContext(_options,_dbLogger.Object)){varhuman=awaitdb.Humans.FindAsync(1001);Assert.NotNull(human);Assert.Equal(1001,human.Id);Assert.Equal("Human1001",human.Name);// Cleanuphuman.Name="Darth Vader";db.Humans.Update(human);awaitdb.SaveChangesAsync();}}[Fact]publicasyncvoidDeleteExistingHuman(){// Givenusing(vardb=newStarWarsContext(_options,_dbLogger.Object)){varhuman10102=newHuman{Id=10102,Name="Human10102"};awaitdb.Humans.AddAsync(human10102);awaitdb.SaveChangesAsync();}// When_humanRepository.Delete(10102);varsaved=await_humanRepository.SaveChangesAsync();// ThenAssert.True(saved);using(vardb=newStarWarsContext(_options,_dbLogger.Object)){vardeletedHuman=awaitdb.Humans.FindAsync(10101);Assert.Null(deletedHuman);}}}}
- TDD (Test First) integration tests for queries atGraphQL Specification by Facebook
namespaceStarWars.Tests.Integration.Api.Controllers{publicclassGraphQLControllerShould{// ...[Fact][Trait("test","integration")]publicasyncvoidExecuteHeroNameQuery(){// Givenconststringquery=@"{ ""query"": ""query HeroNameQuery { hero { name } }"" }";varcontent=newStringContent(query,Encoding.UTF8,"application/json");// Whenvarresponse=await_client.PostAsync("/graphql",content);// Thenresponse.EnsureSuccessStatusCode();varresponseString=awaitresponse.Content.ReadAsStringAsync();Assert.NotNull(responseString);varjobj=JObject.Parse(responseString);Assert.NotNull(jobj);Assert.Equal("R2-D2",(string)jobj["data"]["hero"]["name"]);}[Fact][Trait("test","integration")]publicasyncvoidExecuteHeroNameAndFriendsQuery(){// Givenconststringquery=@"{ ""query"": ""query HeroNameAndFriendsQuery { hero { id name friends { id name } } }"" }";varcontent=newStringContent(query,Encoding.UTF8,"application/json");// Whenvarresponse=await_client.PostAsync("/graphql",content);// Thenresponse.EnsureSuccessStatusCode();varresponseString=awaitresponse.Content.ReadAsStringAsync();Assert.NotNull(responseString);varjobj=JObject.Parse(responseString);Assert.NotNull(jobj);Assert.Equal(3,((JArray)jobj["data"]["hero"]["friends"]).Count);Assert.Equal("Luke Skywalker",(string)jobj["data"]["hero"]["friends"][0]["name"]);Assert.Equal("Han Solo",(string)jobj["data"]["hero"]["friends"][1]["name"]);Assert.Equal("Leia Organa",(string)jobj["data"]["hero"]["friends"][2]["name"]);}[Fact][Trait("test","integration")]publicasyncvoidExecuteNestedQuery(){// Givenconststringquery=@"{ ""query"": ""query NestedQuery { hero { name friends { name appearsIn friends { name } } } }"" }";varcontent=newStringContent(query,Encoding.UTF8,"application/json");// Whenvarresponse=await_client.PostAsync("/graphql",content);// Thenresponse.EnsureSuccessStatusCode();varresponseString=awaitresponse.Content.ReadAsStringAsync();Assert.NotNull(responseString);varjobj=JObject.Parse(responseString);Assert.NotNull(jobj);varluke=jobj["data"]["hero"]["friends"][0];varepisodes=((JArray)luke["appearsIn"]).Select(e=>(string)e).ToArray();Assert.Equal(new[]{"NEWHOPE","EMPIRE","JEDI"},episodes);Assert.Equal(4,((JArray)luke["friends"]).Count);Assert.Equal("Han Solo",(string)luke["friends"][0]["name"]);Assert.Equal("Leia Organa",(string)luke["friends"][1]["name"]);Assert.Equal("C-3PO",(string)luke["friends"][2]["name"]);Assert.Equal("R2-D2",(string)luke["friends"][3]["name"]);}[Fact][Trait("test","integration")]publicasyncvoidExecuteFetchLukeQuery(){// Givenconststringquery=@"{ ""query"": ""query FetchLukeQuery { human(id: ""1000"") { name } } }"" }";varcontent=newStringContent(query,Encoding.UTF8,"application/json");// Whenvarresponse=await_client.PostAsync("/graphql",content);// Thenresponse.EnsureSuccessStatusCode();varresponseString=awaitresponse.Content.ReadAsStringAsync();Assert.NotNull(responseString);varjobj=JObject.Parse(responseString);Assert.NotNull(jobj);Assert.Equal("Luke Skywalker",(string)jobj["data"]["human"]["name"]);}[Fact][Trait("test","integration")]publicasyncvoidExecuteFetchLukeAliased(){// Givenconststringquery=@"{ ""query"": ""query FetchLukeAliased { luke: human(id: ""1000"") { name } } }"" }";varcontent=newStringContent(query,Encoding.UTF8,"application/json");// Whenvarresponse=await_client.PostAsync("/graphql",content);// Thenresponse.EnsureSuccessStatusCode();varresponseString=awaitresponse.Content.ReadAsStringAsync();Assert.NotNull(responseString);varjobj=JObject.Parse(responseString);Assert.NotNull(jobj);Assert.Equal("Luke Skywalker",(string)jobj["data"]["human"]["name"]);}[Fact][Trait("test","integration")]publicasyncvoidExecuteFetchLukeAndLeiaAliased(){// Givenconststringquery=@"{ ""query"": ""query FetchLukeAliased { luke: human(id: ""1000"") { name } leia: human(id: ""1003"") { name } } }"" }";varcontent=newStringContent(query,Encoding.UTF8,"application/json");// Whenvarresponse=await_client.PostAsync("/graphql",content);// Thenresponse.EnsureSuccessStatusCode();varresponseString=awaitresponse.Content.ReadAsStringAsync();Assert.NotNull(responseString);varjobj=JObject.Parse(responseString);Assert.NotNull(jobj);Assert.Equal("Luke Skywalker",(string)jobj["data"]["luke"]["name"]);Assert.Equal("Leia Organa",(string)jobj["data"]["leia"]["name"]);}[Fact][Trait("test","integration")]publicasyncvoidExecuteDuplicateFields(){// Givenconststringquery=@"{ ""query"": ""query DuplicateFields { luke: human(id: ""1000"") { name homePlanet } leia: human(id: ""1003"") { name homePlanet } } }"" }";varcontent=newStringContent(query,Encoding.UTF8,"application/json");// Whenvarresponse=await_client.PostAsync("/graphql",content);// Thenresponse.EnsureSuccessStatusCode();varresponseString=awaitresponse.Content.ReadAsStringAsync();Assert.NotNull(responseString);varjobj=JObject.Parse(responseString);Assert.NotNull(jobj);Assert.Equal("Luke Skywalker",(string)jobj["data"]["luke"]["name"]);Assert.Equal("Tatooine",(string)jobj["data"]["luke"]["homePlanet"]);Assert.Equal("Leia Organa",(string)jobj["data"]["leia"]["name"]);Assert.Equal("Alderaan",(string)jobj["data"]["leia"]["homePlanet"]);}[Fact][Trait("test","integration")]publicasyncvoidExecuteUseFragment(){// Givenconststringquery=@"{ ""query"": ""query UseFragment { luke: human(id: ""1000"") { ...HumanFragment } leia: human(id: ""1003"") { ...HumanFragment } } fragment HumanFragment on Human { name homePlanet } }"" }";varcontent=newStringContent(query,Encoding.UTF8,"application/json");// Whenvarresponse=await_client.PostAsync("/graphql",content);// Thenresponse.EnsureSuccessStatusCode();varresponseString=awaitresponse.Content.ReadAsStringAsync();Assert.NotNull(responseString);varjobj=JObject.Parse(responseString);Assert.NotNull(jobj);Assert.Equal("Luke Skywalker",(string)jobj["data"]["luke"]["name"]);Assert.Equal("Tatooine",(string)jobj["data"]["luke"]["homePlanet"]);Assert.Equal("Leia Organa",(string)jobj["data"]["leia"]["name"]);Assert.Equal("Alderaan",(string)jobj["data"]["leia"]["homePlanet"]);}[Fact][Trait("test","integration")]publicasyncvoidExecuteCheckTypeOfR2(){// Givenconststringquery=@"{ ""query"": ""query CheckTypeOfR2 { hero { __typename name } } }"" }";varcontent=newStringContent(query,Encoding.UTF8,"application/json");// Whenvarresponse=await_client.PostAsync("/graphql",content);// Thenresponse.EnsureSuccessStatusCode();varresponseString=awaitresponse.Content.ReadAsStringAsync();Assert.NotNull(responseString);varjobj=JObject.Parse(responseString);Assert.NotNull(jobj);Assert.Equal("Droid",(string)jobj["data"]["hero"]["__typename"]);Assert.Equal("R2-D2",(string)jobj["data"]["hero"]["name"]);}[Fact][Trait("test","integration")]publicasyncvoidExecuteCheckTypeOfLuke(){// Givenconststringquery=@"{ ""query"": ""query CheckTypeOfLuke { hero(episode: EMPIRE) { __typename name } } }"" }";varcontent=newStringContent(query,Encoding.UTF8,"application/json");// Whenvarresponse=await_client.PostAsync("/graphql",content);// Thenresponse.EnsureSuccessStatusCode();varresponseString=awaitresponse.Content.ReadAsStringAsync();Assert.NotNull(responseString);varjobj=JObject.Parse(responseString);Assert.NotNull(jobj);Assert.Equal("Human",(string)jobj["data"]["hero"]["__typename"]);Assert.Equal("Luke Skywalker",(string)jobj["data"]["hero"]["name"]);}}}