Skip to main content Link Menu Expand (external link) Document Search Copy Copied

STAG-AUIUI-P8VT 2023

Table of contents

  1. 1. Lekce - 10.02.2023
    1. Presentation:
    2. Technical Prerequisites:
    3. Create Project and run it
    4. How to debug (in VS Community)
  2. 2. Lekce - 17.02.2023
    1. Presentation:
    2. Create a build definition using YAML
    3. Create a release pipeline in Azure DevOps
    4. Application urls of teams
  3. 3. Lekce - 24.02.2023
    1. Presentation:
    2. Prerequisites:
    3. Release pipeline triggers
    4. Connect to a MSSQL database
    5. Keyvault names of teams
  4. 4. Lekce - 03.03.2023
    1. Prerequisites:
    2. Postman
    3. Swagger
    4. JWT Authentication & Authorization
    5. More Info:
  5. 5. Lekce - 10.03.2023
    1. Prerequisites:
  6. 6. Lekce - 17.03.2023
  7. 7. Lekce - 24.03.2023
  8. 8. Lekce - 31.03.2023
    1. Presentation:
  9. 9. Lekce - 07.04.2023
  10. 10. Lekce - 13.04.2023
  11. 11. Lekce - 20.04.2023
  12. 12. Lekce - 27.04.2023
  13. 13. Lekce - 05.05.2023
  14. 14. Lekce - 12.05.2023

1. Lekce - 10.02.2023

Overview:

  • Initial presentation
  • Download presentation
  • Setup your own development environment

Presentation:

Presentation

Technical Prerequisites:

Create Project and run it

  1. Create a Blazor webassembly App - ASP.NET Core Hosted
    dotnet new blazorwasm --hosted -n OnlyShare -o .

  2. Or create a React App - ASP.NET Core Hosted
    dotnet new react -n OnlyShare -o .

  3. Maybe you will need to install dev certificates
    dotnet dev-certs https

  4. Run application using
    (Blazor) dotnet run --project .\Server\OnlyShare.Server.csproj
    (React) dotnet run

  5. Or use your ide to run Server project

  6. This will run both backend and frontend as one application

    • The ASP.NET server as a host to both API and frontend
  7. Delete/Change scaffolded code from project

    • ./Server/Pages - Remove whole folder
    • ./Server.Program.cs
      • builder.Services.AddControllersWithViews(); -> builder.Services.AddControllers();
      • builder.Services.AddRazorPages(); - Remove
      • app.MapRazorPages(); - Remove

How to debug (in VS Community)

  1. Open the solution in VS community

  2. Press the debug button ‘https’ in VS

Image

More info

Final code commit of this lecture: 28ab2091744fec2e21b396e4beee56e34e815d8a


2. Lekce - 17.02.2023

We will create a CI/CD pipeline to deploy our application to a staging server.

Overview:

  • Git flow & best practices presentation
  • Create a continuous integration pipeline to build your application.
  • Create a continous delivery pipeline (release) to deploy your application to server.
  • Setup git policies for branches.

Presentation:

Presentation

Prerequisites:

  • Pushed changes into a repository in azure devops

Create a build definition using YAML

We will create a build definition for the application.

Create build pipeline (Aspnet + BlazorWasm):

trigger:
  branches:
    include:
      - master

pool:
  vmImage: windows-latest

variables:
  buildConfiguration: "Release"

steps:
  - task: UseDotNet@2
    displayName: Use Dotnet 7
    inputs:
      version: "7.0.x"

  - task: NuGetToolInstaller@0
    displayName: "Install NuGet"
    inputs:
      versionSpec: 6.0.x
      checkLatest: true

  - task: DotNetCoreCLI@2
    displayName: "Dotnet restore"
    inputs:
      command: restore
      projects: "**/*.csproj"

  - script: dotnet publish Server/OnlyShare.Server.csproj -o $(Build.ArtifactStagingDirectory) --configuration Release /p:EnvironmentName=Development
    displayName: "Dotnet publish OnlyShare"

  - task: PublishBuildArtifacts@1
    inputs:
      PathtoPublish: "$(Build.ArtifactStagingDirectory)"
      ArtifactName: "drop"

Create build pipeline (Aspnet + React):

trigger:
  branches:
    include:
      - master

pool:
  vmImage: windows-latest

variables:
  buildConfiguration: "Release"

steps:
  - task: UseDotNet@2
    displayName: Use Dotnet 7
    inputs:
      version: "7.0.x"

  - task: NuGetToolInstaller@0
    displayName: "Install NuGet"
    inputs:
      versionSpec: 6.0.x
      checkLatest: true

  - task: DotNetCoreCLI@2
    displayName: "Dotnet restore"
    inputs:
      command: restore
      projects: "**/*.csproj"

  - script: dotnet publish OnlyShare.csproj -o $(Build.ArtifactStagingDirectory) --configuration Release /p:EnvironmentName=Development
    displayName: "Dotnet publish OnlyShare"

  - task: PublishBuildArtifacts@1
    inputs:
      PathtoPublish: "$(Build.ArtifactStagingDirectory)"
      ArtifactName: "drop"

Create a release pipeline in Azure DevOps

To host our applications, we will use Azure app service.
To deploy our application, we will create a release pipeline.

Release pipeline:

  1. Create new Release pipeline
  2. Select ‘Azure App Service deployment’
  3. Add artifact
  4. Configure Azure App Service deployment
    • Add subscription (admin needed)
    • Add application (admin needed)
    • Select Package/Folder location (from build)

AzureDevops_Release_Backend_1 AzureDevops_Release_Backend_2

If we did everything correctly, we should have our application deployed and it should look like this:

More Info:

Application urls of teams

  • Amundsen: https://cngroup-utb–2023-os-amundsen.azurewebsites.net
  • Hd kvalita: https://cngroup-utb–2023-os-hd-kvalita.azurewebsites.net
  • Nevime: https://cngroup-utb–2023-os-nevime.azurewebsites.net
  • ORZ: https://cngroup-utb–2023-os-orz.azurewebsites.net
  • Sempa: https://cngroup-utb–2023-os-sempa.azurewebsites.net
  • ToPujde: https://cngroup-utb–2023-os-topujde.azurewebsites.net

How can i get my release running automagically after a new build ?

More info:

Final code commit of this lecture: c8c15d90eb5b5d41ec037f5fb8e9ce4f695a1519

3. Lekce - 24.02.2023

We will create a CI/CD pipeline to deploy our application to a staging server. We will create and connect to local and azure databases.

Overview:

  • ORM & Entity Framework
  • Create migrations and local database
  • Seed database with default data
  • Add connection to Azure database to Release pipeline via connection string stored in Azure Keyvault

Presentation:

Presentation

Prerequisites:

  • Working CI pipeline (build)
  • Working Release pipeline (deploy) [I can see my applications deployed on Azure]
  • Microsoft SQL Server - Download here Developer version
  • Microsoft SQL Server management studio - Download here

Release pipeline triggers

Release triggers in azure devops

Connect to a MSSQL database

You should have MSSQL running, and now you have to create a database for local development.

Install these packages to Server project:

  • Microsoft.EntityFrameworkCore
  • Microsoft.EntityFrameworkCore.Design
  • Microsoft.EntityFrameworkCore.SqlServer
  • Microsoft.EntityFrameworkCore.Tools

Add DB Context

// file: /Database/DataContext.cs

public class DataContext : DbContext
{
    public DataContext(DbContextOptions<DataContext> options) : base(options)
    {

    }
}

Add DB Models - notice the Id property

// file: /Database/Models/WeatherForecast.cs

public class WeatherForecast
{
    public Guid Id { get; set; }

    public DateTime Date { get; set; }

    public int TemperatureC { get; set; }

    public string Summary { get; set; } = string.Empty;
}

Use Model in DBContext

// file: /Database/DataContext.cs

public class DataContext : DbContext
{
    public DataContext(DbContextOptions<DataContext> options) : base(options)
    {

    }

    public DbSet<Models.WeatherForecast> WeatherForecasts { get; set; } = default!;
}

Set database connection string to database on local SQL Server

  • Add to appsettings.json
...
"ConnectionStrings": {
  "Database": "Data Source={name of your local server};Initial Catalog={name of your database};Trusted_Connection=True;Encrypt=False"
}
...

Add DataContext to DI container (startup)

// file: /Program.cs

...

builder.Services.AddDbContext<DataContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("Database")));

...

Add migrations to project

  • Open Package Manager Console
  • Add-Migration InitialMigration

Apply migrations on application start (Automagically)

// file: /Program.cs

...

using var scope = app.Services.CreateScope();
var dataContext = scope.ServiceProvider.GetRequiredService<DataContext>();

if (dataContext == null)
    throw new NullReferenceException("DataContext is not initialized in DI in Program.cs");

dataContext.Database.Migrate();
...

Add Repository and map onto existing WeatherForecast class NOTE: Return type is shared WeatherForecast class, not database model one

// file: /Database/Repositories/IWeatherForecastRepository.cs

public interface IWeatherForecastRepository
{
    IEnumerable<WeatherForecast> GetWeatherForecasts();
}

// file: /Database/Repositories/WeatherForecastRepository.cs

public class WeatherForecastRepository : IWeatherForecastRepository
{
    private readonly DataContext _context;

    public WeatherForecastRepository(DataContext context)
    {
        _context = context;
    }

    public IEnumerable<WeatherForecast> GetWeatherForecasts()
    {
        return _context.WeatherForecasts
            .Select(wf => new WeatherForecast
            {
                Date = DateOnly.FromDateTime(wf.Date),
                Summary = wf.Summary,
                TemperatureC = wf.TemperatureC
            });
    }
}

Add Repository to DI container (startup)

// file: /Program.cs

...

builder.Services.AddScoped<IWeatherForecastRepository, WeatherForecastRepository>();

...

Use Repository in controller to show data from db

// file: /Controllers/WeatherForecastController.cs

[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
    private readonly IWeatherForecastRepository _repository;

    public WeatherForecastController(IWeatherForecastRepository repository)
    {
        _repository = repository;
    }

    [HttpGet(Name = "GetWeatherForecast")]
    public IEnumerable<WeatherForecast> Get()
    {
        return _repository.GetWeatherForecasts();
    }
}

Run the app and test your API

No Data? Let’s seed some default data

// file: /Database/DataContext.cs

...
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    base.OnModelCreating(modelBuilder);

    modelBuilder.Entity<Models.WeatherForecast>().HasData(new Models.WeatherForecast
    {
        Id = Guid.NewGuid(),
        Date = DateTime.Now.AddYears(-2),
        Summary = "Weather 1",
        TemperatureC = 30,
    });
    modelBuilder.Entity<Models.WeatherForecast>().HasData(new Models.WeatherForecast
    {
        Id = Guid.NewGuid(),
        Date = DateTime.Now.AddYears(-1),
        Summary = "Weather 2",
        TemperatureC = 35,
    });
    modelBuilder.Entity<Models.WeatherForecast>().HasData(new Models.WeatherForecast
    {
        Id = Guid.NewGuid(),
        Date = DateTime.Now,
        Summary = "Weather 3",
        TemperatureC = 40,
    });
}
...

Add new migration to project

  • Open Package Manager Console
  • Add-Migration SeedData

Run app again and verify you see the data we just seed into database

Let’s add connectionString saved in Azure keyvault to your Release pipeline

Keyvault names of teams

  • Amundsen: …
  • Hd kvalita: …
  • Nevime: …
  • ORZ: …
  • Sempa: …
  • ToPujde: …

Step 1 Step 2 Step 3 Step 4 Step 5 Step 6 Step 7 Step 8

Create CRUD endpoints to manage data ?

More info:

Final code commit of this lecture: c8c15d90eb5b5d41ec037f5fb8e9ce4f695a1519

4. Lekce - 03.03.2023

UX/UI Presentation

Prerequisites:

  • Working backend database connection using Entity framework
  • Knowing how to use DBContext to CRUD data
  • Installed Postman (or alternative for testing APIs)

Postman

First, test your GET WeatherForecast EP with Postman.

Swagger

Let’s add Swagger to the app so we can easily see what EPs our BE offers

You will need Swashbuckle.AspNetCore package.

// file: Program.cs

...
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

...

if (app.Environment.IsDevelopment())
{
    app.UseWebAssemblyDebugging();

    app.UseSwagger();
    app.UseSwaggerUI();
}
...

JWT Authentication & Authorization

Create a new request/response classes for registration/login

// file: Contracts/LoginRequest

public class LoginRequest
{
    public string Email { get; set; } = string.Empty;

    public string Password { get; set; } = string.Empty;
}
// file: Contracts/LoginResponse.cs

public class LoginResponse
{
    public string Token { get; set; } = string.Empty;
}
// file: Contracts/RegisterRequest.cs

public class RegisterRequest
{
    public string Email { get; set; } = string.Empty;

    public string Password { get; set; } = string.Empty;

    public string PasswordRepeat { get; set; } = string.Empty;

    public string Username { get; set; } = string.Empty;
}
// file: Contracts/RegisterResponse.cs

public class RegisterResponse
{

}

Create new Account Controller

// file: Controllers/AccountController.cs

[ApiController]
[Route("[controller]")]
public class AccountController : ControllerBase
{
    private readonly DataContext _dataContext;

    public AccountController(DataContext dataContext)
    {
        _dataContext = dataContext;
    }

    [HttpPost("[action]")]
    public IActionResult Register(RegisterRequest request)
    {
       ...
    }

    [HttpPost("[action]")]
    public IActionResult Login(LoginRequest request)
    {
        ...
    }
}

Implement Register Method

// file: Controllers/AccountController.cs

[HttpPost("[action]")]
public IActionResult Register(RegisterRequest request)
{
    if (request.Password != request.PasswordRepeat)
        return BadRequest("Zadaná hesla se neshodují");

    if (_dataContext.Users.Any(user => user.Email == request.Email))
        return BadRequest($"Uživatel s emailem {request.Email} je již registrován");

    var (passwordSalt, passwordHash) = CreatePasswordHash(request.Password);

    var user = new User
    {
        Email = request.Email,
        Username = request.Username,
        PasswordHash = passwordHash,
        PasswordSalt = passwordSalt
    };

    _dataContext.Add(user);
    _dataContext.SaveChanges();

    // send email here

    return Ok("Uživatel vytvořen");
}

private static (byte[] passwordSalt, byte[] passwordHash) CreatePasswordHash(string password)
{
    using var hmac = new System.Security.Cryptography.HMACSHA512();
    var passwordSalt = hmac.Key;
    var passwordHash = hmac.ComputeHash(Encoding.UTF8.GetBytes(password));
    return (passwordSalt, passwordHash);
}

Create user Table/Entity

// file: Database/Models/User.cs

public class User
{
    public Guid Id { get; set; }

    public string Email { get; set; } = string.Empty;

    public string Username { get; set; } = string.Empty;

    public byte[] PasswordHash { get; set; } = Array.Empty<byte>();

    public byte[] PasswordSalt { get; set; } = Array.Empty<byte>();
}

And add it to dbContext

// file: Database/DataContext.cs

public DbSet<User> Users { get; set; } = default!;

Create migration and run the app to apply it. You can now test if your register Endpoint works - if your request passess validations, there should be a new user in the DB.

Generate your JwtSecret and put it into appsettings.json (we will use HmacSha256 encryption algorithm, so the secret should be 32 bytes long)

...
"AppSettings": {
  "JwtSecret": "{your secret}"
}
...

Create a class mapping the AppSettings part of appsettings.json

// file: AppSettings.cs

public class AppSettings
{
    public string JwtSecret { get; set; } = String.Empty;
}

Inject AppSettings into AccountController

// file: Controllers/AccountController.cs
...
private readonly IOptions<AppSettings> _options;

public AccountController(DataContext dataContext, IOptions<AppSettings> options)
{
    _dataContext = dataContext;
    _options = options;
}
...

Implement Login Method
You may need to add several usings into the file

// file: Controllers/AccountController.cs

[HttpPost("[action]")]
public IActionResult Login(LoginRequest request)
{
    var user = _dataContext.Users.FirstOrDefault(user => request.Email == user.Email);

    if (user == null)
        return NotFound($"Uživatel ${request.Email} nenalezen.");

    if (VerifyPasswordHash(request.Password, user.PasswordHash, user.PasswordSalt) == false)
        return BadRequest("Neplatné přihlášení");


    var tokenHandler = new JwtSecurityTokenHandler();
    var key = Encoding.ASCII.GetBytes(_options.Value.JwtSecret);
    var tokenDescriptor = new SecurityTokenDescriptor
    {
        Subject = new ClaimsIdentity(new[]
        {
            new Claim("id", user.Id.ToString()),
            new Claim(ClaimTypes.Name, user.Email)
        }),
        Expires = DateTime.UtcNow.AddDays(7),
        SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
    };
    var token = tokenHandler.CreateToken(tokenDescriptor);
    var tokenString = tokenHandler.WriteToken(token);

    if (tokenString == null)
        return BadRequest("Nepodařilo se přihlásit");

    return Ok(new LoginResponse { Token = tokenString });
}

private static bool VerifyPasswordHash(string password, byte[] storedHash, byte[] storedSalt)
{
    if (storedHash.Length != 64)
        throw new ArgumentException("Invalid length of password hash (64 bytes expected).", nameof(storedHash));
    if (storedSalt.Length != 128)
        throw new ArgumentException("Invalid length of password salt (128 bytes expected).", nameof(storedSalt));

    using var hmac = new System.Security.Cryptography.HMACSHA512(storedSalt);
    var computedHash = hmac.ComputeHash(Encoding.UTF8.GetBytes(password));
    for (var i = 0; i < computedHash.Length; i++)
    {
        if (computedHash[i] != storedHash[i]) return false;
    }

    return true;
}

Now you can test that you can log as a registered user and you will get your JWT token, which you can inspect at https://jwt.io.
This token will be used by frontend to prove its identity.

Plug in the ASP.NET authorization for JWT tokens.
You will need Microsoft.AspNetCore.Authentication.JwtBearer package.

// file: Program.cs

var appSettingsSection = builder.Configuration.GetSection("AppSettings");
var jwtSecret = appSettingsSection.Get<AppSettings>().JwtSecret;
builder.Services.Configure<AppSettings>(appSettingsSection);

...

builder.Services.AddAuthentication(options =>
    {
        options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    })
    .AddJwtBearer(options =>
    {
        options.Events = new JwtBearerEvents
        {
            OnTokenValidated = context =>
            {
                var dataContext = context.HttpContext.RequestServices.GetRequiredService<DataContext>();
                var userId = context.Principal.Identity.Name;
                var user = dataContext.Users.FirstOrDefault(user => user.Email == userId);

                if (user == null)
                    context.Fail("Unauthorized");

                return Task.CompletedTask;
            }
        };
        options.RequireHttpsMetadata = false;
        options.SaveToken = true;
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(jwtSecret)),
            ValidateIssuer = false,
            ValidateAudience = false
        };
    });

...

app.UseAuthentication();

Now your app is configured to authenticate using JWT tokens. But you cannot really test this, can you ?

We can add [Authorize] attribute to our controller methods to prevent unauthorized users to use them

// file: Controllers/WeatherForecastController.cs

[HttpPost]
[Authorize]
public void Create(WeatherForecast weatherForecast)
{

Now we can test that only if we send the token in Authorize-Headers we are able to create a weatherforecast, using this endpoint.

How to use token in swagger ? How to store the token in frontend ?

More Info:

Final code commit of this lecture: c8c15d90eb5b5d41ec037f5fb8e9ce4f695a1519


5. Lekce - 10.03.2023

Prerequisites:

  • You can log as a registered user and you will get your JWT token from your backend
  • Knowing what jwt token is

6. Lekce - 17.03.2023

TODO

7. Lekce - 24.03.2023

TODO

8. Lekce - 31.03.2023

Presentation:

Presentation

9. Lekce - 07.04.2023

TODO

10. Lekce - 13.04.2023

TODO

11. Lekce - 20.04.2023

Lego game

12. Lekce - 27.04.2023

TODO

13. Lekce - 05.05.2023

Lecture skipped in favor of coding

14. Lekce - 12.05.2023

Final assessment