-
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
- .net and c# versioning and releases
- sdk and runtime downloads
- recommended IDEs
- Jetbrains Rider
- VS Code + C# Dev Kit
- dotnet:
- Languages: VB.net,C#,F#
- Project Types: Libraries, Console Application, Web Application
- Blazor
- MAUI (Xamarin)
- doc: https://learn.microsoft.com/de-de/dotnet/core/tools/dotnet
- Usefull commands:
- Create files and projects with
dotnet new - add a reference or package with
dotnet add - Build your code with
dotnet build - Run your app with
dotnet runordotnet watch
- Create files and projects with
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
Now we know how to setup a new dotnet project
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
Now we know how to query external http endpoints and work with request results
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
ÌPokemonApiwith 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
Now we know how to use external libraries to improve our code
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.Writeinstead ofConsole.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,...)
Now we know how to create an interactive console application
We want to make the data access code reusable by extracting it to a separate library.
- Add a
libraryproject to your solution and move your code there - Provide a
PokemonServiceclass 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
Now we know how to structure our code to make it reusable
- Add a web api project to your solution and refrence your library
<ProjectReference Include="..\PokemonLib\PokemonLib.csproj" />
- Declare mappings for the
PokemonServicemethods to provide GET endpoints in your apiapp.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
We learned how to create an API project, define endpoints and use dependency injection
- 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
- Add the output caching middleware (Output Caching )
builder.Services.AddOutputCache(); ... app.UseOutputCache();
- Activate caching for your endpoints
.CacheOutput();
- 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
- Provid a new logger instance by using the generic logger interface and inject via constructor
ILogger<PokemonService> logger- Add a
debuglog whenever a pokemon will be requested from the external api and add his name to the log - Additionally add a
warninglog if a pokemon was not found - Configure your
appsettingsfile to enable the debug log level for the PokemonLib namespace
We learned how to make our application more robust, scalabale and with well defined endpoints.
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
IPokemonServiceinterface in your endpoints instead of the concrete type to make them more flexible
builder.Services.AddScoped<IPokemonService, PokemonService>();- Create another
IPokemonServiceimplementation calledPokemonTestService - 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");We learned how to use app configuration to configure our app for deployment environments
- 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(...).
We learned how write and run tests for our code
- Add
Moqas 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
We learned to leverage mocking to effectively write tests for a single layer of our application
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
@injectdirective 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,@oninputand other directives inside your html tags
We learned how to create a web UI with Blazor
Now we will use the Entity Framework (see Docs) as a technology to persist data in a light-weight database
- Add the
Microsoft.EntityFrameworkCoreandMicrosoft.EntityFrameworkCore.Sqlitenuget 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");
});- copy the
PokemonServiceclass to make aPokemonDbCacheServicethat injects thePokemonDbContext - 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
PokemonServicein ourPokemonPageapplication:
builder.Services.AddScoped<IPokemonService, PokemonDbCacheService>();We learned how to use ef core to cache data in a database
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
IServiceProviderinstead 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
GetPokemonListAsyncservice method - for each pokemon query the details and write it to the database (don´t forget to call
SaveChangeson the context) - add the
PokemonSyncJobas a hosted service:
builder.Services.AddHostedService<PokemonSyncJob>();When started, our application now will sync the data before listening for requests.
Know we know how to use hosted services to initially sync data into our database.
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 packagefrom 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
EnsureCreatedcall 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
Now we know how to deal with database changes.