Skip to content

CTBT/dotnet_basics_2

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

34 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation


dotnet + c# Workshop


  • Basic Knowledge

  • Working with the CLI

Coding:

  • Create your first application
  • Calling an external API
  • Learn how to work with external packages
  • Make your app interactive
  • Make your Code reusable
  • Provide your own API
  • Add some features
  • Make your app configurable for different environments
  • Testing your code
  • Creating a simple UI that requests data from the backend
  • Use a database to persist your data

Basic Knowledge


Working with the CLI


Level 1: Create your first application

The goal is to setup a new console application that we can use for displaying data.

  • Create a new solution 'PokemonSolution' (dotnet new ...)
  • Create a new console project 'PokemonConsole' (dotnet new ...)
  • add the project to the solution (dotnet sln)
  • build
  • run

Level 1 completed - ⭐

Now we know how to setup a new dotnet project


Level 2: Calling an external API

The goal is to implement a request to the pokemon api and display the results in the console. The pokeapi pokemon list url can be found in the provided .http file.

  • The easiest way to make a request in .net:
    var client = new HttpClient();
    client.GetAsync(...)
  • Create the necessary model(s) for the pokemon list
  • Use the JsonSerializer class to deserialize the request response content to an object
    JsonSerializer.Deserialize<PokemonList>(...)
  • Display the results in the console (Console.WriteLine) with the help of the ``String.Join'' method

Hint: Be aware of the attribut names and use JsonSerializerOptions to define case insensitivity

Bonus: Fetch exceptions

Level 2 completed - ⭐⭐

Now we know how to query external http endpoints and work with request results


Level 3: Learn how to use the benefits of external nuget packages

The goal ist to simplify and structure your code by using an external nuget package that is not part of the .net sdk Use the Refit library to make http calls to the pokemon api. Refit is a wrapper around the HttpClient class that ueses annotations to define external endpoints.

  • Add the Refit http client library to the project (with your IDE´s package explorer or dotnet add).
  • Create the interface that represents the pokemon api ÌPokemonApi with an annotated method:
    [Get("/api/v2/pokemon?limit=10&offset=0")]
    public void GetPokemonList();
  • Create a refit RestService instance and use it to replace your existing HttpClient request
    RestService.For<IPokemonApi>(host)
  • Call the api again to request details of a (random) pokemon (width, height, moves)
  • display those details in the console

Level 3 completed - ⭐⭐⭐

Now we know how to use external libraries to improve our code


Level 4: Make your app interactive

Use the Spectre.Console nuget package to implement a pokedex in your console. Spectre.Console adds vizualizations and interactive componentes to your console.

  • Add the Spectre.Console console library to the project
  • Use AnsiConsole.Write instead of Console.Write
  • Use the package to display the pokemon names in a selectable list (docs)
AnsiConsole.Prompt(
        new SelectionPrompt<string>()
            .Title("Choose a pokemon:")
            .PageSize(10)
            .MoreChoicesText("[grey](Move up and down to reveal more pokemon)[/]")
            .AddChoices(pokemonList.Results.Select(i=> i.Name)))
  • display information about the selected pokemon
  • bonus: styling (color, panel, table,...)

Level 4 completed - ⭐⭐⭐⭐

Now we know how to create an interactive console application


Level 5: Make your Code reusable

We want to make the data access code reusable by extracting it to a separate library.

  • Add a library project to your solution and move your code there
  • Provide a PokemonService class with public methods to receive the pokemon list and details of a pokemon
  • use a constructor and a private property to provide the IPokemonApi instance

Level 5 completed - ⭐⭐⭐⭐⭐

Now we know how to structure our code to make it reusable


Level 6: Provide your own API

  • Add a web api project to your solution and refrence your library
    <ProjectReference Include="..\PokemonLib\PokemonLib.csproj" />
  • Declare mappings for the PokemonService methods to provide GET endpoints in your api
    app.MapGet("/pokemon", ...)
  • Use the Refit.HttpClientFactorynuget package to add an IPokemonApi instance to the service collection:
  • ... services.AddRefitClient<IPokemonApi>().ConfigureHttpClient(...)

HINT:

  • Use the [FromServices] annotation in your endpoint mapping to inject that dependency into your mapping

    [FromServices] PokemonService service
  • Bonus: Keep your Program.cs clean


Level 6 completed - ⭐⭐⭐⭐⭐⭐

We learned how to create an API project, define endpoints and use dependency injection


Level 7: Improve the api

Provide an openapi UI for your team

  • Reference the Scalar package
  • Map the UI:
app.MapScalarApiReference();
  • add it to your launchsettings:
      "launchBrowser": true,
      "launchUrl": "http://localhost:5063/scalar/v1",
  • test your endpoints with the UI

Make your api scalable with caching

  • Add the output caching middleware (Output Caching )
    builder.Services.AddOutputCache();
    ...
    app.UseOutputCache();
  • Activate caching for your endpoints
    .CacheOutput();

Refine your data types

  • Change your endpoint result types so that the possible status codes are defined
Task<Results<NotFound, Ok<Pokemon>>>
return TypedResults.Ok(pokemon);
  • Have a look into the openapi endpoint description to see the changes

Use the build-in logger of .net to write log messages

  • Provid a new logger instance by using the generic logger interface and inject via constructor
ILogger<PokemonService> logger
  • Add a debug log whenever a pokemon will be requested from the external api and add his name to the log
  • Additionally add a warning log if a pokemon was not found
  • Configure your appsettings file to enable the debug log level for the PokemonLib namespace

Level 7 completed - ⭐⭐⭐⭐⭐⭐⭐

We learned how to make our application more robust, scalabale and with well defined endpoints.


Level 8: Make your app configurable for different environments

The idea is to be able to work with test data insted of the real external api

  • Create an Interface IPokemonService and implement it in the PokemonService
public class PokemonService : IPokemonService
  • Use the IPokemonService interface in your endpoints instead of the concrete type to make them more flexible
builder.Services.AddScoped<IPokemonService, PokemonService>();
  • Create another IPokemonService implementation called PokemonTestService
  • GetPokemonList: generate a fake pokemonlist by using the Range function
Results = Enumerable.Range(1, 2000).Select(i => new PokemonListItem ...
  • GetPokemonDetails: return the requested pokemon with generated test values

  • Configure your dev environment to use that test class

  "ServiceOptions": {
    "UseTestData": false
  }
  • populate your service collection with a IPokemonService type depending on the configuration value
var useTestData = builder.Configuration.GetValue<bool>("ServiceOptions:UseTestData");

Level 8 completed - ⭐⭐⭐⭐⭐⭐⭐⭐

We learned how to use app configuration to configure our app for deployment environments


Level 9 Testing your code

  • Add a xunit test project for the PokemonLib and create a reference to it
  • Add a first test class
  • use the [Fact] data annotation to declare a test method
  • Write a test for the GetPokemonDetails method (for now without mocking) by using an assertion like Assert.True(...).
grafik

Level 9 completed - ⭐⭐⭐⭐⭐⭐⭐⭐⭐

We learned how write and run tests for our code


Level 10 Use mocking to ensure fast and stable tests

  • Add Moq as a mocking library to your test code
  • Rewrite your test of the FetPokemnonDetails method to use a moq of the IPokemonapi.GetPokemonDetails(...) method
  • hint 1: create a mock like this: new Mock<IPokemonApi>()
  • hint 2: use the .Setup(...) method on your mock instance to define your mock behavior

Level 10 completed - ⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐

We learned to leverage mocking to effectively write tests for a single layer of our application


Level 11 - Build your own UI with Blazor

Add a simple ui based on the Blazor Framework to display pokemon data.

  • Add a Blazor Web App "PokemonPage" to your solution (RenderMode: Server, Interactivity Location: Global)
  • Create a razor page that calls the IPokemonService.GetPokemonListAsync() method to display a list of pokemons and provide a search functionality
  • Create a razor page that calls the IPokemonService.GetDetails() method to display pokemon detail information
  • Bonus: Display the pokemon sprite by using it´s id: https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/home/{id}

Hints:

  • Use the @inject directive to make the pokemon service available to your component by injecting the dependency
  • You can always use the @ to reference variables
  • Event handlers can be implemented by using @onchange, @oninput and other directives inside your html tags

Level 11 completed - ⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐

We learned how to create a web UI with Blazor


Level 13 - Persist your data

Now we will use the Entity Framework (see Docs) as a technology to persist data in a light-weight database

Setup the database context for sqlite

  • Add the Microsoft.EntityFrameworkCore and Microsoft.EntityFrameworkCore.Sqlite nuget packages to the PokemonLib
  • Create a new class derived from the base class DbContext:
public class PokemonDbContext: DbContext
{
    public PokemonDbContext(DbContextOptions<PokemonDbContext> options) : base(options){}
    public DbSet<DbPokemon> Pokemons { get; set; }
    public DbSet<DbMove> Moves { get; set; }
}
  • create classes for your database tables DbPokemon and DbMove and define primary keys ([Key] annotation)
  • register the context to your application:
builder.Services.AddDbContext<PokemonDbContext>(options =>
{
    options.UseSqlite("Data Source=pokemon.db");
});

Use the context to cache pokemon data

  • copy the PokemonServiceclass to make a PokemonDbCacheService that injects the PokemonDbContext
  • make sure the database schema is created by calling .Database.EnsureCreated() on the context before using it
  • the data request logic can now be improved by checking the database for data:
if (dbItem is not null)
{

    return new Pokemon()
    {
        Id =  dbItem.Id,
        Name = dbItem.Name,
        Height = dbItem.Height,
        Weight = dbItem.Weight,
        Moves = dbItem.Moves
            .Select(i => new MoveListItem() { Move = new Move(i.Name) })
            .ToList()
    };
}
  • saving a pokemon after requesting the external service works like that:
var dbPokemon = new DbPokemon()
{
  Id = result.Content.Id,
  Name = result.Content.Name,
  Height = result.Content.Height,
  Weight = result.Content.Weight,
  Moves = result.Content.Moves.Select(m => new DbMove() { Name = m.Move.Name }).ToList()
};
_pokemonDbContext.Pokemons.Add(dbPokemon);
await _pokemonDbContext.SaveChangesAsync();
  • now we are ready to replace the existing PokemonService in our PokemonPage application:
builder.Services.AddScoped<IPokemonService, PokemonDbCacheService>();

Level 13 completed - ⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐

We learned how to use ef core to cache data in a database


Level 14: Data import in a background job

The goal is to sync the pokemon data set initially on application startup to decouple it from requests.

  • Create a class PokemonSyncJobthat uses the ÌHostedService interface to execute startup code
  • The new class needs the pokemon api and a database context as dependencies. The database context can´t be refrenced because it does have a different scope lifetime. use the IServiceProvider instead and create a new instance like that:
using var scope = _serviceProvider.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<PokemonDbContext>();
  • Make sure the database is created:
await context.Database.EnsureCreatedAsync(cancellationToken);
  • query a list of pokemons with the GetPokemonListAsync service method
  • for each pokemon query the details and write it to the database (don´t forget to call SaveChanges on the context)
  • add the PokemonSyncJob as a hosted service:
builder.Services.AddHostedService<PokemonSyncJob>();

When started, our application now will sync the data before listening for requests.

Level 14 completed - ⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐

Know we know how to use hosted services to initially sync data into our database.


Level 15: Database schema migrations

We will learn how to create database schema migrations and update a database

  • First we need the entity framework cli tool: dotnet tool install --global dotnet-ef

  • Also we need the Microsoft.EntityFramework.Design package from nuget

  • to see if everything is ready we print a list of available database context: dotnet ef dbcontext list

  • we can always create an sql script for our whole database schema: ``dotnet ef dbcontext script --startup-project ../PokemonPage````

  • now we can create a first migration that includes the current schema: dotnet ef migrations add Initial --startup-project ../PokemonPage

  • to make migrations work we need to change one line of our code. Replace the EnsureCreated call on the context: await context.Database.MigrateAsync(cancellationToken)

  • To create a change let´s create indices for the name attributes of our tables to make queries faster: [Index(nameof(Name))]

  • Again we create a migration: dotnet ef migrations add Indices --startup-project ../PokemonPage

  • at the end we update our local sqlite database to have the newest migration applied dotnet ef database update --startup-project ../PokemonPage

Level 15 completed - ⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐

Now we know how to deal with database changes.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors