27 October, 2022

ASP.NET Core Project

I use Visual Studio Code because it's faster at startup and has a well integrated terminal with the PowerShell console. It is also easier to reproduce the commands by copy/paste than showing the steps to use the graphical UI.

To start a new ASP.NET Core project you need to have the .NET 6 SDK installed. Check if you have the latest version installed with: dotnet --version, currently it's version 6.0.101 called .NET 6.0.

Create and start a project by using templates, here web:

dotnet new web -o TemplateWeb
cd TemplateWeb
dotnet run

Other common templates are webapp webapi nunit classlib. List all available templates with dotnet new

ASP.NET Backend

Create a webapi solution with multiple projects and use MySql as database for Entity Framework:

dotnet new webapi -f net6.0 -o Teach.WebApi
dotnet new nunit -f net6.0 -o Teach.WebApi.Tests
dotnet new classlib -f net6.0 -o Teach.Business
dotnet new nunit -f net6.0 -o Teach.Business.Tests
dotnet new classlib -f net6.0 -o Teach.DataAccess
dotnet new classlib -f net6.0 -o Teach.Domain

cd Teach.WebApi
dotnet add reference ../Teach.Business/Teach.Business.csproj
dotnet add reference ../Teach.DataAccess/Teach.DataAccess.csproj
dotnet add package Pomelo.EntityFrameworkCore.MySql
// for IEmailSender
dotnet add package Microsoft.AspNetCore.Identity.UI --version 6.0.4

cd ../Teach.Business
dotnet add reference ../Teach.DataAccess/Teach.DataAccess.csproj
dotnet add reference ../Teach.Domain/Teach.Domain.csproj
dotnet add package AutoMapper.Extensions.Microsoft.DependencyInjection

cd ../Teach.DataAccess
dotnet add reference ../Teach.Domain/Teach.Domain.csproj
dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Pomelo.EntityFrameworkCore.MySql

cd ../Teach.Domain
dotnet add package Microsoft.AspNetCore.Identity --version 2.2.0

cd ../Teach.Business.Tests
dotnet add reference ../Teach.Business/Teach.Business.csproj
dotnet add reference ../Teach.Domain/Teach.Domain.csproj
dotnet add package Moq --version 4.18.0

cd ../Teach.WebApi.Tests
dotnet add reference ../Teach.WebApi/Teach.WebApi.csproj
dotnet add reference ../Teach.Business/Teach.Business.csproj
dotnet add reference ../Teach.Domain/Teach.Domain.csproj
cd ..
dotnet new sln -n Teach
dotnet sln add Teach.Business/Teach.Business.csproj
dotnet sln add Teach.DataAccess/Teach.DataAccess.csproj
dotnet sln add Teach.Domain/Teach.Domain.csproj
dotnet sln add Teach.WebApi/Teach.WebApi.csproj
dotnet sln add Teach.Business.Tests/Teach.Business.Tests.csproj
dotnet sln add Teach.WebApi.Tests/Teach.WebApi.Tests.csproj
dotnet build

Alternative: dotnet add package MySql.Data.EntityFrameworkCore

dotnet tool install -g Microsoft.Tye --version "0.11.0-alpha.22111.1"

tye run

todo:

Install ASP.NET code generator for scaffolding code:

dotnet tool install -g dotnet-aspnet-codegenerator

Update ASP.NET code generator:

dotnet tool update -g dotnet-aspnet-codegenerator

dotnet aspnet-codegenerator

ASP.NET Web-API

Create a new project:

dotnet new web --name AuthorizationServer

Add dependencies:

cd AuthorizationServer
dotnet add package OpenIddict
dotnet add package OpenIddict.AspNetCore
dotnet add package OpenIddict.EntityFrameworkCore
dotnet add package Pomelo.EntityFrameworkCore.MySql
dotnet add package Microsoft.EntityFrameworkCore.Relational

Add ConnectionString to AuthorizationServer/appsettings.json:

{
  "ConnectionStrings": {
    "ConnectionString": "server=localhost;user id=wisecards;password=YouKnowPassword123;database=wisecards"
  }
}
dotnet ef dbcontext scaffold "server=localhost;user id=wisecards;password=YouKnowPassword123;database=wisecards" "Pomelo.EntityFrameworkCore.MySql"

Add Kestrel DEV endpoint ports:

{
  "Kestrel": {
    "Endpoints": {
      "Http": {
        "Url": "http://localhost:5000"
      },
      "Https": {
        "Url": "https://localhost:7000"
      }
    }
  }
}

Run:

dotnet run --project AuthorizationServer.csproj

Change ASP.NET Core Identity Table Naming

public class TeachDbContext : IdentityDbContext<User, UserUserRole, int>
{
  // ...
  protected override void OnModelCreating(ModelBuilder builder)
  {
    base.OnModelCreating(builder);
    builder.Entity<User>().ToTable("Users").HasKey(x => x.Id);
    builder.Entity<UserUserRole>().ToTable("UserUserRole").HasKey(x => x.Id);
    builder.Entity<IdentityUserLogin<int>>().ToTable("UserLogins").HasKey(x => x.UserId);
    builder.Entity<IdentityUserClaim<int>>().ToTable("UserClaims").HasKey(x => x.Id);
    builder.Entity<IdentityRole>().ToTable("Roles").HasKey(x => x.Id);
    builder.Entity<IdentityRoleClaim<int>>().ToTable("RoleClaims").HasKey(x => x.Id);
    builder.Entity<IdentityUserRole<int>>().ToTable("UserRoles").HasKey(x => new { x.UserId, x.RoleId });
    builder.Entity<IdentityUserToken<int>>().ToTable("UserTokens").HasKey(x => new { x.UserId, x.LoginProvider, x.Name });
  }
}

Cross-Site Request Forgery

Add support for SPA in ASP.NET Core

Progarm.cs

var services = builder.Services;
services.AddMvcCore().AddViews();
services.AddAntiforgery(options => options.HeaderName = "X-XSRF-TOKEN");

// ... after builder.Build();
var antiforgery = app.Services.GetRequiredService<IAntiforgery>();
app.Use((context, next) =>
{
  var path = context.Request.Path.Value;

  if (string.Equals(path, "/", StringComparison.OrdinalIgnoreCase) ||
      string.Equals(path, "/index.html", StringComparison.OrdinalIgnoreCase))
  {
    var tokens = antiforgery.GetAndStoreTokens(context);
    context.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken!, new CookieOptions() { HttpOnly = false });
  }

  return next(context);
});

In controllers:

[ValidateAntiForgeryToken]
public class AccountController : Controller
{
  // ...
}

In SPA:

Get anti-forgerey-token:

GET https://tea.ch/

Grab the cookie 'XSRF-TOKEN' and for each subsequent request, put that in the header X-XSRF-TOKEN.

Like the login-request:

POST https://tea.ch/account/login
Content-Type: application/json
X-XSRF-TOKEN: CfDJ8LJyEjnZE3tJsmURR5M-E9OYcbPWm6YTTbbqLbAgv7FQhSkhhj870sl9SvYwvWSZv6fiJWGR_ZiL18J-JkdnUmgnkJ_RtmrhVTaUugDJ1Tb0VCiAsCr6qt019UJqjq9gJ4jEX5m3RL9OypcuuDYNm8eYwwqiQWbSW71Lx0aG75xmCEyPH8a089L6bK3UOExA_A

{
  "username": "5a",
  "password": "1.12",
  "returnUrl": "https://tea.ch"
}

Unit Test

Test a Controller using Moq to mock data-access.

using Microsoft.AspNetCore.Mvc;
using Moq;
using NUnit.Framework;
using System.Threading.Tasks;
using Wisecards.Business.Services;
using Wisecards.DataAccess;
using Wisecards.Domain;
using Wisecards.WebApi.Controllers;

namespace Wisecards.WebApi.Tests;

public class DecksControllerTest
{
  private Mock<IDeckAccess> deckAccessMock = new Mock<IDeckAccess>();

  [SetUp]
  public void Setup()
  {
  }

  [Test]
  public async Task DeleteDeck()
  {
    // arrange
    var deck = new Deck();
    deckAccessMock.Setup(m => m.GetById(It.IsAny<int>())).Returns(Task.FromResult<Deck?>(deck));
    deckAccessMock.Setup(m => m.Update(It.IsAny<Deck>())).Returns(Task.FromResult<int>(1));
    var deckService = new DeckService(null!, deckAccessMock.Object);
    var controller = new DecksController(deckService);

    // act
    var result = await controller.DeleteDeckById(1);

    // assert
    Assert.NotNull(result);
    Assert.IsTrue(deck.IsDeleted);
    Assert.AreEqual(202, (result?.Result as OkResult)?.StatusCode);
  }
}

[1] https://github.com/openiddict/openiddict-core

[2] https://github.com/PomeloFoundation/Pomelo.EntityFrameworkCore.MySql

[3] https://docs.microsoft.com/en-us/aspnet/core/security/authentication/?view=aspnetcore-6.0