StarWars

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

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

Examples

Roadmap

  • [x] Basic
    • [x] Simple tutorial (step/screenshot/code)
    • [ ] Detailed tutorial (steps explanation)
    • [x] 3-Layers (Api, Core, Data) architecture
    • [x] DDD (Domain Driven Design) hexagonal architecture
    • [x] Dependency Inversion (deafult ASP.NET Core IoC container)
    • [x] GraphQL controller
    • [x] In Memory ‘Droid’ Repository
    • [x] Entity Framework ‘Droid’ Repository
    • [x] Automatic database creation
    • [x] Seed database data
    • [x] EF Migrations
    • [x] Graph*i*QL
    • [x] Unit Tests
    • [x] Visual Studio 2017 RC upgrade
    • [x] Integration Tests
    • [x] Logs
    • [x] Code Coverage
    • [ ] Continous Integration
  • [ ] Advanced
    • [x] Full ‘Star Wars’ database (Episodes, Characters, Planets, Humans etc.)
    • [x] Base/generic repository
    • [x] Visual Studio 2017 RTM upgrade
    • [x] 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 “`csharp using GraphQL.Types; using StarWars.Core.Models;

namespace StarWars.Api.Models { public class DroidType : ObjectGraphType { 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
```csharp
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’ “`csharp 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 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](https://cloud.githubusercontent.com/assets/8171434/22866705/17985b54-f17b-11e6-848c-6482b45e4934.png)

* Create 'IDroidRepository' interface
```csharp
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’ “`csharp 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 _droids = new List { 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
```csharp
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 “`csharp 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
```json
{
  "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 “`csharp 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
```csharp
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 “`csharp // … public void ConfigureServices(IServiceCollection services) { services.AddMvc();

    services.AddTransient(); services.AddTransient(); services.AddDbContext(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](https://cloud.githubusercontent.com/assets/8171434/22923521/1fb046da-f2a2-11e6-8e56-c00661e27318.png)

* Final test using Postman
![postman-test-query](https://cloud.githubusercontent.com/assets/8171434/22866705/17985b54-f17b-11e6-848c-6482b45e4934.png)

#### Entity Framework Migrations

* Add 'Microsoft.EntityFrameworkCore.Design' NuGet package to 'StarWars.Data' project
![ef-design-nuget](https://cloud.githubusercontent.com/assets/8171434/22964859/fde4e42a-f35a-11e6-89ad-1b5dfeda4e67.png)

* Add 'Microsoft.EntityFrameworkCore.Tools.DotNet' NuGet package to 'StarWars.Data' project
![ef-tools-dotnet-nuget](https://cloud.githubusercontent.com/assets/8171434/22964861/ff9afdf4-f35a-11e6-8e42-87ed30009877.png)

* Add tools section in project.json (StarWars.Data)
```json
"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 csharp 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 json "buildOptions": { "emitEntryPoint": true }
  • Run migrations command from the console dotnet ef migrations add Inital -o .\EntityFramework\Migrations dotnet-ef-migrations

Grahp*i*QL

  • 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’ “`javascript 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](https://cloud.githubusercontent.com/assets/8171434/22973925/2a6cd7ee-f380-11e6-89b8-b27fe68a6d1b.png)

* Configure 'After Build' step in 'Task Runner Explorer'
![after-build-task-runner-explorer](https://cloud.githubusercontent.com/assets/8171434/22974769/25727768-f384-11e6-96ca-8670b67b805c.png)
```json
  "-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 “`javascript 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(, document.getElementById(‘app’));

    * app.css
    ```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 Graph*i*QL 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’ “`json { “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
```csharp
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 “`csharp 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() .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
    ```csharp
    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'
    ```csharp
    // ...
    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
    ```csharp
    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](https://cloud.githubusercontent.com/assets/8171434/23065886/0d9aae72-f518-11e6-8f60-f9c226c6b49a.png)

* Upgrade of 'StarWars.Tests.Unit' failed, so I had to remove all project dependencies and reload it
```json
{
  "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’ “`csharp // … private IHostingEnvironment Env { get; set; }

    public class Startup { // … Env = env; }

    public void ConfigureServices(IServiceCollection services) { // … if (Env.IsEnvironment(“Test”)) { services.AddDbContext(options => options.UseInMemoryDatabase(databaseName: “StarWars”)); } else { services.AddDbContext(options => options.UseSqlServer(Configuration[“ConnectionStrings:StarWarsDatabaseConnection”])); } // … } // … “`

  • Create integration test for GraphQL query (POST) “`csharp 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
```csharp
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 “`csharp 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>(); // … using (var context = new StarWarsContext(options, dbLogger.Object)) { // … } // … var repoLogger = new Mock>(); _droidRepository = new DroidRepository(starWarsContext, repoLogger.Object); } } }


* Enjoy console logs
![console-logs-1](https://cloud.githubusercontent.com/assets/8171434/23155559/6b17afe8-f813-11e6-88e0-2923270f4ee8.png)
![console-logs-2](https://cloud.githubusercontent.com/assets/8171434/23155802/68f8e604-f814-11e6-90a7-44d09e851a51.png)

#### Code Coverage

* Install OpenCover NuGet package
![open-cover-nuget](https://cloud.githubusercontent.com/assets/8171434/23206746/247aa694-f8ef-11e6-9b14-d0ade0453b94.png)

* 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.
```xml
<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

  • Configure CI using VSTS (Visual Studio Team Services).
    TODO: 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

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

  • TDD (Test First) integration tests for queries at GraphQL Specification by Facebook “`csharp 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.Conte
    

Top Contributors

JacekKosciesza