Skip to content

GraphQL 'Star Wars' example using GraphQL for .NET, ASP.NET Core, Entity Framework Core

License

Notifications You must be signed in to change notification settings

jamie-tillman/StarWars

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

63 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

GraphQL 'Star Wars' example using GraphQL for .NET, ASP.NET Core, Entity Framework Core

Build Status

Examples

Roadmap

  • 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
    • ...

Tutorials

Basic

  • Create 'StarWars' empty solution empty-solution

  • Add 'ASP.NET Core Web Application (.NET Core)' project named 'StarWars.Api' aspnet-core-web-application

  • Select Web API template aspnet-core-web-api

  • Update all NuGet packages update-all-nuget-packages

  • Update project.json with correct runtime

"runtimes": {
   "win10-x64": { }
}
  • Install GraphQL NuGet package install-graphql-nuget-package

  • Create 'StarWars.Core' project starwars-core-project

  • Create 'Droid' model

namespace StarWars.Core.Models
{
    public class Droid
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }
}
  • Create 'DroidType' model
using GraphQL.Types;
using StarWars.Core.Models;

namespace StarWars.Api.Models
{
    public class DroidType : ObjectGraphType<Droid>
    {
        public DroidType()
        {
            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
using GraphQL.Types;
using StarWars.Core.Models;

namespace StarWars.Api.Models
{
    public class StarWarsQuery : ObjectGraphType
    {
        public StarWarsQuery()
        {
            Field<DroidType>(
              "hero",
              resolve: context => new Droid { Id = 1, Name = "R2-D2" }
            );
        }
    }
}
  • Create 'GraphQLQuery' model
namespace StarWars.Api.Models
{
    public class GraphQLQuery
    {
        public string OperationName { get; set; }
        public string NamedQuery { get; set; }
        public string Query { get; set; }
        public string Variables { get; set; }
    }
}
  • Create 'GraphQLController'
using GraphQL;
using GraphQL.Types;
using Microsoft.AspNetCore.Mvc;
using StarWars.Api.Models;
using System.Threading.Tasks;

namespace StarWars.Api.Controllers
{
    [Route("graphql")]
    public class GraphQLController : Controller
    {
        [HttpPost]
        public async Task<IActionResult> Post([FromBody] GraphQLQuery query)
        {
            var schema = new Schema { Query = new StarWarsQuery() };

            var result = await new DocumentExecuter().ExecuteAsync(_ =>
            {
                _.Schema = schema;
                _.Query = query.Query;

            }).ConfigureAwait(false);

            if (result.Errors?.Count > 0)
            {
                return BadRequest();
            }

            return Ok(result);
        }
    }
}
  • Test using Postman postman-test-query

  • Create 'IDroidRepository' interface

using StarWars.Core.Models;
using System.Threading.Tasks;

namespace StarWars.Core.Data
{
    public interface IDroidRepository
    {
        Task<Droid> Get(int id);
    }
}
  • Create 'StarWars.Data' project starwars-data-project

  • Create in memory 'DroidRepository'

using StarWars.Core.Data;
using System.Collections.Generic;
using System.Threading.Tasks;
using StarWars.Core.Models;
using System.Linq;

namespace StarWars.Data.InMemory
{
    public class DroidRepository : IDroidRepository
    {
        private List<Droid> _droids = new List<Droid> {
            new Droid { Id = 1, Name = "R2-D2" }
        };

        public Task<Droid> Get(int id)
        {
            return Task.FromResult(_droids.FirstOrDefault(droid => droid.Id == id));
        }
    }
}
  • Use 'IDroidRepository' in StarWarsQuery
using GraphQL.Types;
using StarWars.Core.Data;

namespace StarWars.Api.Models
{
    public class StarWarsQuery : ObjectGraphType
    {
        private IDroidRepository _droidRepository { get; set; }

        public StarWarsQuery(IDroidRepository _droidRepository)
        {
            Field<DroidType>(
              "hero",
              resolve: context => _droidRepository.Get(1)
            );
        }
    }
}
  • Update creation of StarWarsQuery in GraphQLController
// ...
public async Task<IActionResult> Post([FromBody] GraphQLQuery query)
{
    var schema = new Schema { Query = new StarWarsQuery(new DroidRepository()) };
// ...
  • Test using Postman

  • Configure dependency injection in Startup.cs

// ...
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();

    services.AddTransient<StarWarsQuery>();
    services.AddTransient<IDroidRepository, DroidRepository>();
}
// ...
  • Use constructor injection of StarWarsQuery in GraphQLController
// ...
public class GraphQLController : Controller
{
    private StarWarsQuery _starWarsQuery { get; set; }

    public GraphQLController(StarWarsQuery starWarsQuery)
    {
        _starWarsQuery = starWarsQuery;
    }

    [HttpPost]
    public async Task<IActionResult> Post([FromBody] GraphQLQuery query)
    {
        var schema = new Schema { Query = _starWarsQuery };
// ...
  • Add Microsoft.EntityFrameworkCore and Microsoft.EntityFrameworkCore.SqlServer Nuget packages to StarWars.Data project entity-framework-core-nuget entity-framework-sql-server-provider-nuget

  • Create StarWarsContext

using Microsoft.EntityFrameworkCore;
using StarWars.Core.Models;

namespace StarWars.Data.EntityFramework
{
    public class StarWarsContext : DbContext
    {
        public StarWarsContext(DbContextOptions options)
            : base(options)
        {
            Database.EnsureCreated();
        }

        public DbSet<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
using StarWars.Core.Data;
using System.Threading.Tasks;
using StarWars.Core.Models;
using Microsoft.EntityFrameworkCore;

namespace StarWars.Data.EntityFramework.Repositories
{
    public class DroidRepository : IDroidRepository
    {
        private StarWarsContext _db { get; set; }

        public DroidRepository(StarWarsContext db)
        {
            _db = db;
        }

        public Task<Droid> Get(int id)
        {
            return _db.Droids.FirstOrDefaultAsync(droid => droid.Id == id);
        }
    }
}
  • Create seed data as an extension to StarWarsContext
using StarWars.Core.Models;
using System.Linq;

namespace StarWars.Data.EntityFramework.Seed
{
    public static class StarWarsSeedData
    {
        public static void EnsureSeedData(this StarWarsContext db)
        {
            if (!db.Droids.Any())
            {
                var droid = new Droid
                {
                    Name = "R2-D2"
                };
                db.Droids.Add(droid);
                db.SaveChanges();
            }
        }
    }
}
  • Configure dependency injection and run data seed in Startup.cs
// ...
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();

    services.AddTransient<StarWarsQuery>();
    services.AddTransient<IDroidRepository, DroidRepository>();
    services.AddDbContext<StarWarsContext>(options => 
        options.UseSqlServer(Configuration["ConnectionStrings:StarWarsDatabaseConnection"])
    );
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env,
                        ILoggerFactory loggerFactory, StarWarsContext db)
{
    loggerFactory.AddConsole(Configuration.GetSection("Logging"));
    loggerFactory.AddDebug();

    app.UseMvc();

    db.EnsureSeedData();
}
// ...
  • Run application and make sure database is created ssms-starwars-database-created

  • Final test using Postman postman-test-query

Entity Framework Migrations

  • Add 'Microsoft.EntityFrameworkCore.Design' NuGet package to 'StarWars.Data' project ef-design-nuget

  • Add 'Microsoft.EntityFrameworkCore.Tools.DotNet' NuGet package to 'StarWars.Data' project ef-tools-dotnet-nuget

  • 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
    namespace StarWars.Data.EntityFramework.Workaround
    {
        // WORKAROUND: https://docs.efproject.net/en/latest/miscellaneous/cli/dotnet.html#targeting-class-library-projects-is-not-supported
        public static class Program
        {
            public static void Main() { }
        }
    }
    • Add build option in project.json
    "buildOptions": {
        "emitEntryPoint": true
    }
  • Run migrations command from the console
dotnet ef migrations add Inital -o .\EntityFramework\Migrations

dotnet-ef-migrations

GrahpiQL

  • Add NPM configuration file 'package.json' to StarWars.Api project npm-configuration-file

  • 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'
var webpack = require('webpack');
var ExtractTextPlugin = require('extract-text-webpack-plugin');

var output = './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: [
      new ExtractTextPlugin('style.css', { allChunks: true })
    ]
};
  • Install 'NPM Task Runner' extension npm-task-runner

  • Configure 'After Build' step in 'Task Runner Explorer' after-build-task-runner-explorer

  "-vs-binding": { "AfterBuild": [ "start" ] }
  • Add 'Get' action to GraphQL controller and GraphiQL view (~/Views/GraphQL/index.cshtml)
// ...
public class GraphQLController : Controller
{
    // ...
    [HttpGet]
    public IActionResult Index()
    {
        return View();
    }    
// ...
}
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width" />
    <title>GraphiQL</title>
    <link rel="stylesheet" href="~/style.css" />
</head>
<body>
    <div id="app"></div>
    <script src="~/bundle.js" type="text/javascript"></script>
</body>
</html>
  • Add GraphiQL scripts and styles (app.js and app.css to ~/GraphiQL)

    • app.js
    import React from 'react';
    import ReactDOM from 'react-dom';
    import GraphiQL from 'graphiql';
    import fetch from 'isomorphic-fetch';
    import 'graphiql/graphiql.css';
    import './app.css';
    
    function graphQLFetcher(graphQLParams) {
        return fetch(window.location.origin + '/graphql', {
            method: 'post',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(graphQLParams)
        }).then(response => response.json());
    }
    
    ReactDOM.render(<GraphiQL fetcher={graphQLFetcher}/>, document.getElementById('app'));
    • app.css
    html, body {
        height: 100%;
        margin: 0;
        overflow: hidden;
        width: 100%;
    }
    
    #app {
        height: 100vh;
    }
  • Add static files support

    • Add 'Microsoft.AspNetCore.StaticFiles' NuGet static-files-nuget

    • Update configuration in 'Startup.cs'

    // ...
    public void Configure(IApplicationBuilder app, IHostingEnvironment env,
                              ILoggerFactory loggerFactory, StarWarsContext db)
    {
        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 bundles-created-by-webpack

  • Run project and enjoy GraphiQL graphiql

Unit Tests

  • Create 'Class Library (.NET Core)' type 'StarWars.Tests.Unit' project unit-tests-project

  • Install 'xunit' NuGet package in StarWars.Tests.Unit project xunit-nuget

  • Install 'dotnet-test-xunit' NuGet package in StarWars.Tests.Unit project dotnet-test-xunit-nuget

  • 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
using StarWars.Data.InMemory;
using Xunit;

namespace StarWars.Tests.Unit.Data.InMemory
{
    public class DroidRepositoryShould
    {
        private readonly DroidRepository _droidRepository;
        public DroidRepositoryShould()
        {
            // Given
            _droidRepository = new DroidRepository();
        }

        [Fact]
        public async void ReturnR2D2DroidGivenIdOf1()
        {
            // When
            var droid = await _droidRepository.Get(1);

            // Then
            Assert.NotNull(droid);
            Assert.Equal("WRONG_NAME", droid.Name);
        }
    }
}
  • Build and make sure that test is discovered by 'Test Explorer' test-explorer-first-unit-test

  • Run test - it should fail (we want to make sure that we are testing the right thing) first-unit-test-fail

  • Fix test

// ...
[Fact]
public async void ReturnR2D2DroidGivenIdOf1()
{
    // When
    var droid = await _droidRepository.Get(1);

    // Then
    Assert.NotNull(droid);
    Assert.Equal("R2-D2", droid.Name);
}
// ...
  • Run test again - it should pass first-unit-test-pass

  • Install 'Moq' NuGet package moq-nuget

  • Install 'Microsoft.EntityFrameworkCore.InMemory' NuGet package ef-in-memory-nuget

  • Add reference to 'StarWars.Core' in project.json

{
  "dependencies": {
    "StarWars.Core": {
      "target": "project"
    }
  }
}
  • Create EF droid repository unit test
using Microsoft.EntityFrameworkCore;
using StarWars.Core.Models;
using StarWars.Data.EntityFramework;
using StarWars.Data.EntityFramework.Repositories;
using Xunit;

namespace StarWars.Tests.Unit.Data.EntityFramework.Repositories
{
    public class DroidRepositoryShould
    {
        private readonly DroidRepository _droidRepository;
        public DroidRepositoryShould()
        {
            // Given
            // https://docs.microsoft.com/en-us/ef/core/miscellaneous/testing/in-memory
            var options = new DbContextOptionsBuilder<StarWarsContext>()
                .UseInMemoryDatabase(databaseName: "StarWars")
                .Options;
            using (var context = new StarWarsContext(options))
            {
                context.Droids.Add(new Droid { Id = 1, Name = "R2-D2" });
                context.SaveChanges();
            }
            var starWarsContext = new StarWarsContext(options);
            _droidRepository = new DroidRepository(starWarsContext);
        }

        [Fact]
        public async void ReturnR2D2DroidGivenIdOf1()
        {
            // When
            var droid = await _droidRepository.Get(1);

            // Then
            Assert.NotNull(droid);
            Assert.Equal("R2-D2", droid.Name);
        }
    }
}
  • Create GraphQLController unit test
    • First refactor controller to be more testable by using constructor injection
    using GraphQL;
    using GraphQL.Types;
    using Microsoft.AspNetCore.Mvc;
    using StarWars.Api.Models;
    using System.Threading.Tasks;
    
    namespace StarWars.Api.Controllers
    {
        [Route("graphql")]
        public class GraphQLController : Controller
        {
            private IDocumentExecuter _documentExecuter { get; set; }
            private ISchema _schema { get; set; }
    
            public GraphQLController(IDocumentExecuter documentExecuter, ISchema schema)
            {
                _documentExecuter = documentExecuter;
                _schema = schema;
            }
    
            [HttpGet]
            public IActionResult Index()
            {
                return View();
            }
    
            [HttpPost]
            public async Task<IActionResult> Post([FromBody] GraphQLQuery query)
            {
                var executionOptions = new ExecutionOptions { Schema = _schema, Query = query.Query };
                var result = await _documentExecuter.ExecuteAsync(executionOptions).ConfigureAwait(false);
    
                if (result.Errors?.Count > 0)
                {
                    return BadRequest(result.Errors);
                }
    
                return Ok(result);
            }
        }
    }
    • Configure dependency injection in 'Startup.cs'
    // ...
    public void ConfigureServices(IServiceCollection services)
    {
        // ...        
        services.AddTransient<IDocumentExecuter, DocumentExecuter>();
        var sp = services.BuildServiceProvider();
        services.AddTransient<ISchema>(_ => new Schema { Query = sp.GetService<StarWarsQuery>() });
    }
    // ...
    • Create test for 'Index' and 'Post' actions
    using GraphQL;
    using GraphQL.Types;
    using Microsoft.AspNetCore.Mvc;
    using Moq;
    using StarWars.Api.Controllers;
    using StarWars.Api.Models;
    using System.Threading.Tasks;
    using Xunit;
    
    namespace StarWars.Tests.Unit.Api.Controllers
    {
        public class GraphQLControllerShould
        {
            private GraphQLController _graphqlController { get; set; }
    
            public GraphQLControllerShould()
            {
                // Given
                var documentExecutor = new Mock<IDocumentExecuter>();
                documentExecutor.Setup(x => x.ExecuteAsync(It.IsAny<ExecutionOptions>())).Returns(Task.FromResult(new ExecutionResult()));
                var schema = new Mock<ISchema>();
                _graphqlController = new GraphQLController(documentExecutor.Object, schema.Object);
            }
    
            [Fact]
            public void ReturnNotNullViewResult()
            {
                // When
                var result = _graphqlController.Index() as ViewResult;
    
                // Then
                Assert.NotNull(result);
                Assert.IsType<ViewResult>(result);
            }
    
            [Fact]
            public async void ReturnNotNullExecutionResult()
            {
                // Given
                var query = new GraphQLQuery { Query = @"{ ""query"": ""query { hero { id name } }""" };
    
                // When
                var result = await _graphqlController.Post(query);
    
                // Then
                Assert.NotNull(result);
                var okObjectResult =  Assert.IsType<OkObjectResult>(result);
                var executionResult = okObjectResult.Value;
                Assert.NotNull(executionResult);
            }
        }
    }

Visual Studio 2017 RC upgrade

  • Open solution in VS 2017 and let the upgrade tool do the job vs-2017-rc-upgrade

  • 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) xunit-runner-visualstudio-nuget

  • Install (xunit.runner.visualstudio) dependency (Microsoft.DotNet.InternalAbstractions) microsoft-dotnet-internal-abstractions-nuget

Integration Tests

  • Create 'xUnit Test Project (.NET Core)' type 'StarWars.Tests.Integration' project integration-tests-project

  • Change target framework from 'netcoreapp1.0' to 'netcoreapp1.1'

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>    
    <TargetFramework>netcoreapp1.1</TargetFramework>
  </PropertyGroup>
  <!--...-->
</Project>
  • Install 'Microsoft.AspNetCore.TestHost' NuGet package test-host-nuget

  • Use EF in memory database for 'Test' evironment

    • Install 'Microsoft.EntityFrameworkCore.InMemory' NuGet package ef-in-memory-nuget-api-project

    • Configure it in 'Startup.cs'

    // ...
    private IHostingEnvironment Env { get; set; }
    
    public class Startup
    {
        // ...
        Env = env;
    }
    
    public void ConfigureServices(IServiceCollection services)
    {
        // ...
        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)

using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.TestHost;
using StarWars.Api;
using System.Net.Http;
using System.Text;
using Xunit;

namespace StarWars.Tests.Integration.Api.Controllers
{
    public class GraphQLControllerShould
    {
        private readonly TestServer _server;
        private readonly HttpClient _client;

        public GraphQLControllerShould()
        {
            _server = new TestServer(new WebHostBuilder()
                .UseEnvironment("Test")
                .UseStartup<Startup>()
            );
            _client = _server.CreateClient();
        }

        [Fact]
        public async void ReturnR2D2Droid()
        {
            // Given
            var query = @"{
                ""query"": ""query { hero { id name } }""
            }";
            var content = new StringContent(query, Encoding.UTF8, "application/json");

            // When
            var response = await _client.PostAsync("/graphql", content);

            // Then
            response.EnsureSuccessStatusCode();
            var responseString = await response.Content.ReadAsStringAsync();
            Assert.Contains("R2-D2", responseString);
        }
    }
}

Logs

  • Make sure that logger is configured in Startup.cs
public void Configure(IApplicationBuilder app, IHostingEnvironment env,
                              ILoggerFactory loggerFactory, StarWarsContext db)
{
    loggerFactory.AddConsole(Configuration.GetSection("Logging"));
    loggerFactory.AddDebug();
    // ...
}
  • Override ToString method of GraphQLQuery class
public override string ToString()
{
    var builder = new StringBuilder();
    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}");
    }

    return builder.ToString();
}
  • Add logger to GraphQLController
public class GraphQLController : Controller
{
    // ...
    private readonly ILogger _logger;

    public GraphQLController(IDocumentExecuter documentExecuter, ISchema schema, ILogger<GraphQLController> logger)
    {
        // ...
        _logger = logger;
    }

    [HttpGet]
    public IActionResult Index()
    {
        _logger.LogInformation("Got request for GraphiQL. Sending GUI back");
        return View();
    }

    [HttpPost]
    public async Task<IActionResult> Post([FromBody] GraphQLQuery query)
    {
        // ...
        if (result.Errors?.Count > 0)
        {
            _logger.LogError("GraphQL errors: {0}", result.Errors);
            return BadRequest(result);
        }

        _logger.LogDebug("GraphQL execution result: {result}", JsonConvert.SerializeObject(result.Data));
        return Ok(result);
    }
}
  • Add logger to DroidRepository
namespace StarWars.Data.EntityFramework.Repositories
{
    public class DroidRepository : IDroidRepository
    {
        private StarWarsContext _db { get; set; }
        private readonly ILogger _logger;

        public DroidRepository(StarWarsContext db, ILogger<DroidRepository> logger)
        {
            _db = db;
            _logger = logger;
        }

        public Task<Droid> Get(int id)
        {
            _logger.LogInformation("Get droid with id = {id}", id);
            return _db.Droids.FirstOrDefaultAsync(droid => droid.Id == id);
        }
    }
}
  • Add logger to StarWarsContext
namespace StarWars.Data.EntityFramework
{
    public class StarWarsContext : DbContext
    {
        public readonly ILogger _logger;

        public StarWarsContext(DbContextOptions options, ILogger<StarWarsContext> logger)
            : base(options)
        {
            _logger = logger;
            // ...
        }
    }
}
  • Add logger to StarWarsSeedData
namespace StarWars.Data.EntityFramework.Seed
{
    public static class StarWarsSeedData
    {
        public static void EnsureSeedData(this StarWarsContext db)
        {
            db._logger.LogInformation("Seeding database");
            if (!db.Droids.Any())
            {
                db._logger.LogInformation("Seeding droids");
                // ...
            }
        }
    }
}
  • Fix controller unit test
public class GraphQLControllerShould
{
    public GraphQLControllerShould()
    {
        // ...
        var logger = new Mock<ILogger<GraphQLController>>();
        _graphqlController = new GraphQLController(documentExecutor.Object, schema.Object, logger.Object);
    }
    // ...
}
  • Fix repository unit test
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Moq;
using StarWars.Core.Models;
using StarWars.Data.EntityFramework;
using StarWars.Data.EntityFramework.Repositories;
using Xunit;

namespace StarWars.Tests.Unit.Data.EntityFramework.Repositories
{
    public class DroidRepositoryShould
    {
        public DroidRepositoryShould()
        {
            var dbLogger = new Mock<ILogger<StarWarsContext>>();
            // ...
            using (var context = new StarWarsContext(options, dbLogger.Object))
            {
                // ...
            }
            // ...
            var repoLogger = new Mock<ILogger<DroidRepository>>();
            _droidRepository = new DroidRepository(starWarsContext, repoLogger.Object);
        }
    }
}
  • Enjoy console logs console-logs-1 console-logs-2

Code Coverage

  • Install OpenCover NuGet package open-cover-nuget

  • 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.
<Project Sdk="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

open-cover-unit-tests-results

  • Install 'ReportGenerator' NuGet package report-generator-nuget

  • Create simple script (unit-tests.bat)

mkdir coverage\unit
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
ReportGenerator.exe -reports:coverage/unit/coverage.xml -targetdir:coverage/unit -verbosity:Error
start .\coverage\unit\index.htm
  • Enjoy HTML based code coverage report open-cover-html-report-results

Continous Integration

  • Create new project in VSTS (Visual Studio Team Services) vsts-new-project

  • 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? #3311 vsts-new-build-definition vsts-new-build-definition-github-hosted-vs2017

  • Add new GitHub service connection vsts-new-github-service-connection

  • Setup repository vsts-setup-repository

  • Switch to "New Build Editor" vsts-new-build-editor

  • Setup build process (tasks, build steps) vsts-setup-build-steps

  • Setup projects in Test build step

**/Tests/StarWars.Tests.Unit/StarWars.Tests.Unit.csproj;**/Tests/StarWars.Tests.Integration/StarWars.Tests.Integration.csproj

vsts-setup-projects-test-build-setp

  • Queue build. Make sure it succeeded and executed unit and integration tests. vsts-queue-build vsts-build-succeeded

  • Enable build badge (after save you will see link to build status image). vsts-enable-badge

Advanced

Full 'Star Wars' database (see Facebook GraphQL and GraphQL.js)

  • Create models
namespace StarWars.Core.Models
{
    public class Episode
    {
        public int  Id  { get; set; }
        public string Title { get; set; }
        public virtual ICollection<CharacterEpisode> CharacterEpisodes { get; set; }
    }
}
namespace StarWars.Core.Models
{
    public class Planet
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public ICollection<Human> Humans { get; set; }
    }
}
namespace StarWars.Core.Models
{
    public class Character
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public virtual ICollection<CharacterEpisode> CharacterEpisodes { get; set; }
        public virtual ICollection<CharacterFriend> CharacterFriends { get; set; }
        public virtual ICollection<CharacterFriend> FriendCharacters { get; set; }
    }
}
namespace StarWars.Core.Models
{
    public class CharacterEpisode
    {
        public int CharacterId { get; set; }
        public Character Character { get; set; }

        public int EpisodeId { get; set; }
        public Episode Episode { get; set; }
    }
}
namespace StarWars.Core.Models
{
    public class CharacterFriend
    {
        public int CharacterId { get; set; }
        public Character Character { get; set; }

        public int FriendId { get; set; }
        public Character Friend { get; set; }
    }
}
namespace StarWars.Core.Models
{
    public class Droid : Character
    {
        public string PrimaryFunction { get; set; }
    }
}
namespace StarWars.Core.Models
{
    public class Human : Character
    {
        public Planet HomePlanet { get; set; }
    }
}
  • Update StarWarsContext
namespace StarWars.Data.EntityFramework
{
    public class StarWarsContext : DbContext
    {
        // ...
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            // https://docs.microsoft.com/en-us/ef/core/modeling/relationships
            // http://stackoverflow.com/questions/38520695/multiple-relationships-to-the-same-table-in-ef7core

            // episodes
            modelBuilder.Entity<Episode>().HasKey(c => c.Id);
            modelBuilder.Entity<Episode>().Property(e => e.Id).ValueGeneratedNever();

            // planets
            modelBuilder.Entity<Planet>().HasKey(c => c.Id);
            modelBuilder.Entity<Planet>().Property(e => e.Id).ValueGeneratedNever();

            // characters
            modelBuilder.Entity<Character>().HasKey(c => c.Id);
            modelBuilder.Entity<Character>().Property(e => e.Id).ValueGeneratedNever();

            // characters-friends
            modelBuilder.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-episodes
            modelBuilder.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);

            // humans
            modelBuilder.Entity<Human>().HasOne(h => h.HomePlanet).WithMany(p => p.Humans);
        }

        public virtual DbSet<Episode> Episodes { get; set; }
        public virtual DbSet<Planet> Planets { get; set; }
        public virtual DbSet<Character> Characters { get; set; }
        public virtual DbSet<CharacterFriend> CharacterFriends { get; set; }
        public virtual DbSet<CharacterEpisode> CharacterEpisodes { get; set; }
        public virtual DbSet<Droid> Droids { get; set; }
        public virtual DbSet<Human> Humans { get; set; }
    }
}
  • Update database seed data
namespace StarWars.Data.EntityFramework.Seed
{
    public static class StarWarsSeedData
    {
        public static void EnsureSeedData(this StarWarsContext db)
        {
            db._logger.LogInformation("Seeding database");

            // episodes
            var newhope = new Episode { Id = 4, Title = "NEWHOPE" };
            var empire = new Episode { Id = 5, Title = "EMPIRE" };
            var jedi = new Episode { Id = 6, Title = "JEDI" };
            var episodes = new List<Episode>
            {
                newhope,
                empire,
                jedi,
            };
            if (!db.Episodes.Any())
            {
                db._logger.LogInformation("Seeding episodes");
                db.Episodes.AddRange(episodes);
                db.SaveChanges();
            }

            // planets
            var tatooine = new Planet { Id = 1, Name = "Tatooine" };
            var alderaan = new Planet { Id = 2, Name = "Alderaan" };
            var planets = new List<Planet>
            {
                tatooine,
                alderaan
            };
            if (!db.Planets.Any())
            {
                db._logger.LogInformation("Seeding planets");
                db.Planets.AddRange(planets);
                db.SaveChanges();
            }

            // humans
            var luke = new Human
            {
                Id = 1000,
                Name = "Luke Skywalker",
                CharacterEpisodes = new List<CharacterEpisode>
                {
                    new CharacterEpisode { Episode = newhope },
                    new CharacterEpisode { Episode = empire },
                    new CharacterEpisode { Episode = jedi }
                },
                HomePlanet = tatooine
            };
            var vader = new Human
            {
                Id = 1001,
                Name = "Darth Vader",
                CharacterEpisodes = new List<CharacterEpisode>
                {
                    new CharacterEpisode { Episode = newhope },
                    new CharacterEpisode { Episode = empire },
                    new CharacterEpisode { Episode = jedi }
                },
                HomePlanet = tatooine
            };
            var han = new Human
            {
                Id = 1002,
                Name = "Han Solo",
                CharacterEpisodes = new List<CharacterEpisode>
                {
                    new CharacterEpisode { Episode = newhope },
                    new CharacterEpisode { Episode = empire },
                    new CharacterEpisode { Episode = jedi }
                },
                HomePlanet = tatooine
            };
            var leia = new Human
            {
                Id = 1003,
                Name = "Leia Organa",
                CharacterEpisodes = new List<CharacterEpisode>
                {
                    new CharacterEpisode { Episode = newhope },
                    new CharacterEpisode { Episode = empire },
                    new CharacterEpisode { Episode = jedi }
                },
                HomePlanet = alderaan
            };
            var tarkin = new Human
            {
                Id = 1004,
                Name = "Wilhuff Tarkin",
                CharacterEpisodes = new List<CharacterEpisode>
                {
                    new CharacterEpisode { Episode = newhope }                    
                },
            };
            var humans = new List<Human>
            {
                luke,
                vader,
                han,
                leia,
                tarkin
            };
            if (!db.Humans.Any())
            {
                db._logger.LogInformation("Seeding humans");                
                db.Humans.AddRange(humans);
                db.SaveChanges();
            }

            // droids
            var threepio = new Droid
            {
                Id = 2000,
                Name = "C-3PO",
                CharacterEpisodes = new List<CharacterEpisode>
                {
                    new CharacterEpisode { Episode = newhope },
                    new CharacterEpisode { Episode = empire },
                    new CharacterEpisode { Episode = jedi }
                },
                PrimaryFunction = "Protocol"
            };
            var artoo = new Droid
            {
                Id = 2001,
                Name = "R2-D2",
                CharacterEpisodes = new List<CharacterEpisode>
                {
                    new CharacterEpisode { Episode = newhope },
                    new CharacterEpisode { Episode = empire },
                    new CharacterEpisode { Episode = jedi }
                },
                PrimaryFunction = "Astromech"
            };
            var droids = new List<Droid>
            {
                threepio,
                artoo
            };
            if (!db.Droids.Any())
            {
                db._logger.LogInformation("Seeding droids");
                db.Droids.AddRange(droids);
                db.SaveChanges();
            }

            // update character's friends
            luke.CharacterFriends = new List<CharacterFriend>
            {
                new CharacterFriend { Friend = han },
                new CharacterFriend { Friend = leia },
                new CharacterFriend { Friend = threepio },
                new CharacterFriend { Friend = artoo }
            };
            vader.CharacterFriends = new List<CharacterFriend>
            {
                new CharacterFriend { Friend = tarkin }
            };
            han.CharacterFriends = new List<CharacterFriend>
            {
                new CharacterFriend { Friend = luke },
                new CharacterFriend { Friend = leia },
                new CharacterFriend { Friend = artoo }
            };
            leia.CharacterFriends = new List<CharacterFriend>
            {
                new CharacterFriend { Friend = luke },
                new CharacterFriend { Friend = han },
                new CharacterFriend { Friend = threepio },
                new CharacterFriend { Friend = artoo }
            };
            tarkin.CharacterFriends = new List<CharacterFriend>
            {
                new CharacterFriend { Friend = vader }
            };
            threepio.CharacterFriends = new List<CharacterFriend>
            {
                new CharacterFriend { Friend = luke },
                new CharacterFriend { Friend = han },
                new CharacterFriend { Friend = leia },
                new CharacterFriend { Friend = artoo }
            };
            artoo.CharacterFriends = new List<CharacterFriend>
            {
                new CharacterFriend { Friend = luke },
                new CharacterFriend { Friend = han },
                new CharacterFriend { Friend = leia }
            };
            var characters = new List<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();
            }
        }
    }
}
  • Add 'Microsoft.EntityFrameworkCore.Tools' NuGet ef-tools-nuget

  • Set 'StarWars.Data' as a StartUp project

  • Add 'Full' migrations ef-full-migration

  • Update database ef-update-database-full ef-update-database-full-ssms

  • Set 'StarWars.Api' as a StartUp project

  • Run 'StarWars.Api' to seed database ef-seed-full-database seeded-full-database-smss

  • Create integration test checking EF configuration and seeded data

namespace StarWars.Tests.Integration.Data.EntityFramework
{
    public class StarWarsContextShould
    {
        [Fact]
        public async void ReturnR2D2Droid()
        {
            // Given
            using (var db = new StarWarsContext())
            {
                // When
                var r2d2 = await db.Droids
                    .Include("CharacterEpisodes.Episode")
                    .Include("CharacterFriends.Friend")
                    .FirstOrDefaultAsync(d => d.Id == 2001);

                // Then
                Assert.NotNull(r2d2);
                Assert.Equal("R2-D2", r2d2.Name);
                Assert.Equal("Astromech", r2d2.PrimaryFunction);
                var episodes = r2d2.CharacterEpisodes.Select(e => e.Episode.Title);
                Assert.Equal(new string[] { "NEWHOPE", "EMPIRE", "JEDI" }, episodes);
                var friends = r2d2.CharacterFriends.Select(e => e.Friend.Name);
                Assert.Equal(new string[] { "Luke Skywalker", "Han Solo", "Leia Organa" }, friends);
            }
        }
    }
}
  • Make sure all tests pass all-tests-pass-full-database

  • Update StarWarsQuery with new hero ("R2-D2") ID (2001)

namespace StarWars.Api.Models
{
    public class StarWarsQuery : ObjectGraphType
    {
        // ...
        public StarWarsQuery(IDroidRepository _droidRepository)
        {
            Field<DroidType>(
              "hero",
              resolve: context => _droidRepository.Get(2001)
            );
        }
    }
}
  • Make sure application still works graphiql-full-database

Base/generic repository

  • Create generic entity interface
namespace StarWars.Core.Data
{
    public interface IEntity<TKey>
    {
        TKey Id { get; set; }
    }
}
  • Update models to inherit from IEntity interface (integer based id)
namespace StarWars.Core.Models
{
    public class Character : IEntity<int>
    {
        // ...
    }
}
namespace StarWars.Core.Models
{
    public class Episode : IEntity<int>
    {
        // ...
    }
}
namespace StarWars.Core.Models
{
    public class Planet : IEntity<int>
    {
        // ...
    }
}
  • Create base/generic repository interface
namespace StarWars.Core.Data
{
    public interface IBaseRepository<TEntity, in TKey>
        where TEntity : class
    {
        Task<List<TEntity>> GetAll();
        Task<TEntity> Get(TKey id);
        TEntity Add(TEntity entity);
        void AddRange(IEnumerable<TEntity> entities);
        void Delete(TKey id);
        void Update(TEntity entity);
        Task<bool> SaveChangesAsync();
    }
}
  • Create Entity Framework base/generic repository
namespace StarWars.Data.EntityFramework.Repositories
{
    public abstract class BaseRepository<TEntity, TKey> : IBaseRepository<TEntity, TKey>
        where TEntity : class, IEntity<TKey>, new()
    {
        protected DbContext _db;
        protected readonly ILogger _logger;

        protected BaseRepository() { }

        protected BaseRepository(DbContext db, ILogger logger)
        {
            _db = db;
            _logger = logger;
        }

        public virtual Task<List<TEntity>> GetAll()
        {
            return _db.Set<TEntity>().ToListAsync();
        }

        public virtual Task<TEntity> Get(TKey id)
        {
            _logger.LogInformation("Get {type} with id = {id}", typeof(TEntity).Name, id);
            return _db.Set<TEntity>().SingleOrDefaultAsync(c => c.Id.Equals(id));
        }

        public virtual TEntity Add(TEntity entity)
        {
            _db.Set<TEntity>().Add(entity);
            return entity;
        }

        public void AddRange(IEnumerable<TEntity> entities)
        {
            _db.Set<TEntity>().AddRange(entities);
        }

        public virtual void Delete(TKey id)
        {
            var entity = new TEntity { Id = id };
            _db.Set<TEntity>().Attach(entity);
            _db.Set<TEntity>().Remove(entity);
        }

        public virtual async Task<bool> SaveChangesAsync()
        {
            return (await _db.SaveChangesAsync()) > 0;
        }

        public virtual void Update(TEntity entity)
        {
            _db.Set<TEntity>().Attach(entity);
            _db.Entry(entity).State = EntityState.Modified;
        }
    }
}
  • Refactor EF Droid repository
namespace StarWars.Core.Data
{
    public interface IDroidRepository : IBaseRepository<Droid, int> { }
}
namespace StarWars.Data.EntityFramework.Repositories
{
    public class DroidRepository : BaseRepository<Droid, int>, IDroidRepository
    {
        public DroidRepository() { }

        public DroidRepository(StarWarsContext db, ILogger<DroidRepository> logger)
            : base(db, logger)
        {
        }
    }
}
  • Refactor in-memeory Droid repository
namespace StarWars.Data.InMemory
{
    public class DroidRepository : IDroidRepository
    {
        private readonly ILogger _logger;

        public DroidRepository() { }

        public DroidRepository(ILogger<DroidRepository> logger)
        {
            _logger = logger;
        }

        private List<Droid> _droids = new List<Droid> {
            new Droid { Id = 1, Name = "R2-D2" }
        };

        public Task<Droid> Get(int id)
        {
            _logger.LogInformation("Get droid with id = {id}", id);
            return Task.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

Visual Studio 2017 RTM upgrade

  • Update all NuGet packages for the solution (especially .NET Core v1.1.1) vs-2017-rtm-nugets-update

  • 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. consolidate-netcore-app

Install-Package Microsoft.NETCore.App

package-manager-console-netcore-app-upgrade

  • Fix 'DroidType' unit test (capitalization of field names)
namespace StarWars.Tests.Unit.Api.Models
{
    public class DroidTypeShould
    {
        [Fact]
        public void HaveIdAndNameFields()
        {
            // When
            var droidType = new DroidType();

            // Then
            Assert.NotNull(droidType);
            Assert.True(droidType.HasField("Id"));
            Assert.True(droidType.HasField("Name"));
        }
    }
}

Repositories

  • Create rest of the repositories (Character, Episode, Human, Planet)
namespace StarWars.Core.Data
{
    public interface IHumanRepository : IBaseRepository<Human, int> { }
}
namespace StarWars.Data.EntityFramework.Repositories
{
    public class HumanRepository : BaseRepository<Human, int>, IHumanRepository
    {
        public HumanRepository() { }

        public HumanRepository(StarWarsContext db, ILogger<HumanRepository> logger)
            : base(db, logger)
        {
        }
    }
}
  • Update base repository with 'include' versions
namespace StarWars.Core.Data
{
    public interface IBaseRepository<TEntity, in TKey>
        where TEntity : class
    {
        // ...
        Task<List<TEntity>> GetAll(string include);
        Task<List<TEntity>> GetAll(IEnumerable<string> includes);
        
        // ...

        Task<TEntity> Get(TKey id, string include);
        Task<TEntity> Get(TKey id, IEnumerable<string> includes);
        // ...
    }
}
namespace StarWars.Data.EntityFramework.Repositories
{
    public abstract class BaseRepository<TEntity, TKey> : IBaseRepository<TEntity, TKey>
        where TEntity : class, IEntity<TKey>, new()
    {
        // ...
        public Task<List<TEntity>> GetAll(string include)
        {
            _logger.LogInformation("Get all {type}s (including {include})", typeof(TEntity).Name, include);
            return _db.Set<TEntity>().Include(include).ToListAsync();
        }

        public Task<List<TEntity>> GetAll(IEnumerable<string> includes)
        {
            _logger.LogInformation("Get all {type}s (including [{includes}])", typeof(TEntity).Name, string.Join(",", includes));
            var query = _db.Set<TEntity>().AsQueryable();
            query = includes.Aggregate(query, (current, include) => current.Include(include));
            return query.ToListAsync();
        }

        // ...

        public Task<TEntity> Get(TKey id, string include)
        {
            _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));
        }

        public Task<TEntity> Get(TKey id, IEnumerable<string> includes)
        {
            _logger.LogInformation("Get {type} with id = {id} (including [{include}])", typeof(TEntity).Name, id, string.Join(",", includes));
            var query = _db.Set<TEntity>().AsQueryable();
            query = includes.Aggregate(query, (current, include) => current.Include(include));
            return query.SingleOrDefaultAsync(c => c.Id.Equals(id));
        }

        // ...
    }
}
  • Create repositories CRUD unit tests
namespace StarWars.Tests.Unit.Data.EntityFramework.Repositories
{
    public class HumanRepositoryShould
    {
        private readonly HumanRepository _humanRepository;
        private DbContextOptions<StarWarsContext> _options;
        private Mock<ILogger<StarWarsContext>> _dbLogger;
        public HumanRepositoryShould()
        {
            // Given
            _dbLogger = new Mock<ILogger<StarWarsContext>>();
            _options = new DbContextOptionsBuilder<StarWarsContext>()
                .UseInMemoryDatabase(databaseName: "StarWars_HumanRepositoryShould")
                .Options;
            using (var context = new StarWarsContext(_options, _dbLogger.Object))
            {
                context.EnsureSeedData();
            }
            var starWarsContext = new StarWarsContext(_options, _dbLogger.Object);
            var repoLogger = new Mock<ILogger<HumanRepository>>();
            _humanRepository = new HumanRepository(starWarsContext, repoLogger.Object);
        }

        [Fact]
        public async void ReturnLukeGivenIdOf1000()
        {
            // When
            var luke = await _humanRepository.Get(1000);

            // Then
            Assert.NotNull(luke);
            Assert.Equal("Luke Skywalker", luke.Name);
        }

        [Fact]
        public async void ReturnLukeFriendsAndEpisodes()
        {
            // When
            var character = await _humanRepository.Get(1000, includes: new[] { "CharacterEpisodes.Episode", "CharacterFriends.Friend" });

            // Then
            Assert.NotNull(character);
            Assert.NotNull(character.CharacterEpisodes);
            var episodes = character.CharacterEpisodes.Select(e => e.Episode.Title);
            Assert.Equal(new[] { "NEWHOPE", "EMPIRE", "JEDI" }, episodes);
            Assert.NotNull(character.CharacterFriends);
            var friends = character.CharacterFriends.Select(e => e.Friend.Name);
            Assert.Equal(new[] { "Han Solo", "Leia Organa", "C-3PO", "R2-D2" }, friends);
        }

        [Fact]
        public async void ReturnLukesHomePlanet()
        {
            // When
            var luke = await _humanRepository.Get(1000, include: "HomePlanet");

            // Then
            Assert.NotNull(luke);
            Assert.NotNull(luke.HomePlanet);
            Assert.Equal("Tatooine", luke.HomePlanet.Name);
        }

        [Fact]
        public async void AddNewHuman()
        {
            // Given
            var human10101 = new Human { Id = 10101, Name = "Human10101" };

            // When
            _humanRepository.Add(human10101);
            var saved = await _humanRepository.SaveChangesAsync();

            // Then
            Assert.True(saved);
            using (var db = new StarWarsContext(_options, _dbLogger.Object))
            {
                var human = await db.Humans.FindAsync(10101);
                Assert.NotNull(human);
                Assert.Equal(10101, human.Id);
                Assert.Equal("Human10101", human.Name);

                // Cleanup
                db.Humans.Remove(human);
                await db.SaveChangesAsync();
            }
        }

        [Fact]
        public async void UpdateExistingHuman()
        {
            // Given
            var vader = await _humanRepository.Get(1001);
            vader.Name = "Human1001";

            // When
            _humanRepository.Update(vader);
            var saved = await _humanRepository.SaveChangesAsync();

            // Then
            Assert.True(saved);
            using (var db = new StarWarsContext(_options, _dbLogger.Object))
            {
                var human = await db.Humans.FindAsync(1001);
                Assert.NotNull(human);
                Assert.Equal(1001, human.Id);
                Assert.Equal("Human1001", human.Name);

                // Cleanup
                human.Name = "Darth Vader";
                db.Humans.Update(human);
                await db.SaveChangesAsync();
            }
        }

        [Fact]
        public async void DeleteExistingHuman()
        {
            // Given
            using (var db = new StarWarsContext(_options, _dbLogger.Object))
            {
                var human10102 = new Human { Id = 10102, Name = "Human10102" };
                await db.Humans.AddAsync(human10102);
                await db.SaveChangesAsync();
            }

            // When
            _humanRepository.Delete(10102);
            var saved = await _humanRepository.SaveChangesAsync();

            // Then
            Assert.True(saved);
            using (var db = new StarWarsContext(_options, _dbLogger.Object))
            {
                var deletedHuman = await db.Humans.FindAsync(10101);
                Assert.Null(deletedHuman);
            }
        }
    }
}
  • Check test results test-explorer-repositories-crud-tests code-coverage-repositories-crud-tests

GraphQL queries

namespace StarWars.Tests.Integration.Api.Controllers
{
    public class GraphQLControllerShould
    {
        // ...
        [Fact]
        [Trait("test", "integration")]
        public async void ExecuteHeroNameQuery()
        {
            // Given
            const string query = @"{
                ""query"": 
                    ""query HeroNameQuery {
                        hero {
                            name
                        }
                    }""
            }";
            var content = new StringContent(query, Encoding.UTF8, "application/json");

            // When
            var response = await _client.PostAsync("/graphql", content);

            // Then
            response.EnsureSuccessStatusCode();
            var responseString = await response.Content.ReadAsStringAsync();
            Assert.NotNull(responseString);
            var jobj = JObject.Parse(responseString);
            Assert.NotNull(jobj);
            Assert.Equal("R2-D2", (string)jobj["data"]["hero"]["name"]);
        }

        [Fact]
        [Trait("test", "integration")]
        public async void ExecuteHeroNameAndFriendsQuery()
        {
            // Given
            const string query = @"{
                ""query"":
                    ""query HeroNameAndFriendsQuery {
                        hero {
                            id
                            name
                            friends {
                                id
                                name
                            }
                        }
                    }""
            }";
            var content = new StringContent(query, Encoding.UTF8, "application/json");

            // When
            var response = await _client.PostAsync("/graphql", content);

            // Then
            response.EnsureSuccessStatusCode();
            var responseString = await response.Content.ReadAsStringAsync();
            Assert.NotNull(responseString);
            var jobj = 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")]
        public async void ExecuteNestedQuery()
        {
            // Given
            const string query = @"{
                ""query"":
                    ""query NestedQuery {
                        hero {
                            name
                            friends {
                                name
                                appearsIn
                                friends {
                                    name
                                }
                            }
                        }
                    }""
            }";
            var content = new StringContent(query, Encoding.UTF8, "application/json");

            // When
            var response = await _client.PostAsync("/graphql", content);

            // Then
            response.EnsureSuccessStatusCode();
            var responseString = await response.Content.ReadAsStringAsync();
            Assert.NotNull(responseString);
            var jobj = JObject.Parse(responseString);
            Assert.NotNull(jobj);
            var luke = jobj["data"]["hero"]["friends"][0];
            var episodes = ((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")]
        public async void ExecuteFetchLukeQuery()
        {
            // Given
            const string query = @"{
                ""query"":
                    ""query FetchLukeQuery {
                        human(id: ""1000"") {
                            name
                        }
                    }
                }""
            }";
            var content = new StringContent(query, Encoding.UTF8, "application/json");

            // When
            var response = await _client.PostAsync("/graphql", content);

            // Then
            response.EnsureSuccessStatusCode();
            var responseString = await response.Content.ReadAsStringAsync();
            Assert.NotNull(responseString);
            var jobj = JObject.Parse(responseString);
            Assert.NotNull(jobj);
            Assert.Equal("Luke Skywalker", (string)jobj["data"]["human"]["name"]);
        }

        [Fact]
        [Trait("test", "integration")]
        public async void ExecuteFetchLukeAliased()
        {
            // Given
            const string query = @"{
                ""query"":
                    ""query FetchLukeAliased {
                        luke: human(id: ""1000"") {
                            name
                        }
                    }
                }""
            }";
            var content = new StringContent(query, Encoding.UTF8, "application/json");

            // When
            var response = await _client.PostAsync("/graphql", content);

            // Then
            response.EnsureSuccessStatusCode();
            var responseString = await response.Content.ReadAsStringAsync();
            Assert.NotNull(responseString);
            var jobj = JObject.Parse(responseString);
            Assert.NotNull(jobj);
            Assert.Equal("Luke Skywalker", (string)jobj["data"]["human"]["name"]);
        }

        [Fact]
        [Trait("test", "integration")]
        public async void ExecuteFetchLukeAndLeiaAliased()
        {
            // Given
            const string query = @"{
                ""query"":
                    ""query FetchLukeAliased {
                        luke: human(id: ""1000"") {
                            name
                        }
                        leia: human(id: ""1003"") {
                            name
                        }
                    }
                }""
            }";
            var content = new StringContent(query, Encoding.UTF8, "application/json");

            // When
            var response = await _client.PostAsync("/graphql", content);

            // Then
            response.EnsureSuccessStatusCode();
            var responseString = await response.Content.ReadAsStringAsync();
            Assert.NotNull(responseString);
            var jobj = 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")]
        public async void ExecuteDuplicateFields()
        {
            // Given
            const string query = @"{
                ""query"":
                    ""query DuplicateFields {
                        luke: human(id: ""1000"") {
                            name
                            homePlanet
                        }
                        leia: human(id: ""1003"") {
                            name
                            homePlanet
                        }
                    }
                }""
            }";
            var content = new StringContent(query, Encoding.UTF8, "application/json");

            // When
            var response = await _client.PostAsync("/graphql", content);

            // Then
            response.EnsureSuccessStatusCode();
            var responseString = await response.Content.ReadAsStringAsync();
            Assert.NotNull(responseString);
            var jobj = 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")]
        public async void ExecuteUseFragment()
        {
            // Given
            const string query = @"{
                ""query"":
                    ""query UseFragment {
                        luke: human(id: ""1000"") {
                            ...HumanFragment
                        }
                        leia: human(id: ""1003"") {
                            ...HumanFragment
                        }
                    }

                    fragment HumanFragment on Human {
                        name
                        homePlanet
                    }
                }""
            }";
            var content = new StringContent(query, Encoding.UTF8, "application/json");

            // When
            var response = await _client.PostAsync("/graphql", content);

            // Then
            response.EnsureSuccessStatusCode();
            var responseString = await response.Content.ReadAsStringAsync();
            Assert.NotNull(responseString);
            var jobj = 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")]
        public async void ExecuteCheckTypeOfR2()
        {
            // Given
            const string query = @"{
                ""query"":
                    ""query CheckTypeOfR2 {
                        hero {
                            __typename
                            name
                        } 
                    }
                }""
            }";
            var content = new StringContent(query, Encoding.UTF8, "application/json");

            // When
            var response = await _client.PostAsync("/graphql", content);

            // Then
            response.EnsureSuccessStatusCode();
            var responseString = await response.Content.ReadAsStringAsync();
            Assert.NotNull(responseString);
            var jobj = 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")]
        public async void ExecuteCheckTypeOfLuke()
        {
            // Given
            const string query = @"{
                ""query"":
                    ""query CheckTypeOfLuke {
                       hero(episode: EMPIRE) {
                            __typename
                            name
                       }
                    }
                }""
            }";
            var content = new StringContent(query, Encoding.UTF8, "application/json");

            // When
            var response = await _client.PostAsync("/graphql", content);

            // Then
            response.EnsureSuccessStatusCode();
            var responseString = await response.Content.ReadAsStringAsync();
            Assert.NotNull(responseString);
            var jobj = JObject.Parse(responseString);
            Assert.NotNull(jobj);
            Assert.Equal("Human", (string)jobj["data"]["hero"]["__typename"]);
            Assert.Equal("Luke Skywalker", (string)jobj["data"]["hero"]["name"]);
        }
    }
}

facebook-spec-queries-tdd-integration-tests-failed

About

GraphQL 'Star Wars' example using GraphQL for .NET, ASP.NET Core, Entity Framework Core

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • C# 97.5%
  • Batchfile 1.3%
  • JavaScript 1.1%
  • CSS 0.1%