Overview

Request pipeline

Every request flows through the same ASP.NET Core Minimal API pipeline, regardless of endpoint:

flowchart TD
    Client(["Client"]) --> CORS["CORS middleware"]
    CORS --> AuthN["Authentication<br/>(ApiKeyAuthenticationHandler)"]
    AuthN --> AuthZ["Authorization<br/>(RequireAuthorization on writes only)"]
    AuthZ --> Endpoint["Endpoint delegate<br/>(Endpoints/*.cs)"]
    Endpoint --> Service["Service<br/>(Services/*.cs)"]
    Service --> Validation["DataAnnotations validation<br/>(Common/Validation.cs)"]
    Service --> EFCore["Entity Framework Core<br/>(AppDbContext)"]
    EFCore --> DB[("SQLite / MariaDB")]

    Endpoint -.exception.-> ExceptionHandler["GlobalExceptionHandler<br/>→ RFC 7807 ProblemDetails"]

AuthN always runs, but only endpoints marked .RequireAuthorization() (the write operations) actually enforce it — see Security.

Project structure

flowchart LR
    subgraph src
        Api["BookmarksManager.Api<br/>(Minimal API host)"]
        MigSqlite["BookmarksManager.Migrations.Sqlite<br/>(EF migrations, Sqlite)"]
        MigMaria["BookmarksManager.Migrations.MariaDb<br/>(EF migrations, MariaDB)"]
    end
    subgraph tests
        ApiTests["BookmarksManager.Api.Tests<br/>(xUnit, unit + integration)"]
    end
    seed["seed/<br/>(SeedImporter + sample.json)"]

    MigSqlite -->|references, design-time only| Api
    MigMaria -->|references, design-time only| Api
    ApiTests -->|references| Api
    Api -.compiles.-> seed

Inside BookmarksManager.Api:

Folder Responsibility
Endpoints/ Minimal API route groups (BookmarkEndpoints, FolderEndpoints, TagEndpoints, HealthEndpoints) — routing and RequireAuthorization() placement only
Services/ Business logic and EF Core queries (BookmarkService, FolderService)
Models/ EF Core entities (Bookmark, Folder, Tag, BookmarkTag)
Dtos/ Request/response records used at the HTTP boundary
Data/ AppDbContext, DatabaseProvider (Sqlite/MariaDb switch), design-time AppDbContextFactory
Auth/ ApiKeyAuthenticationHandler and scheme constants
Common/ Cross-cutting concerns: validation, custom exceptions, GlobalExceptionHandler

Why two migrations projects?

EF Core migrations are provider-specific (SQLite and MariaDB generate different SQL), so each provider gets its own migrations assembly. DatabaseProvider.Configure points EF Core at the matching assembly based on Database:Provider, and AppDbContextFactory (used only by dotnet ef migrations add at design time) picks the same assembly via an EF_PROVIDER environment variable:

1
2
3
4
5
6
7
EF_PROVIDER=Sqlite  dotnet ef migrations add InitialCreate \
  --project src/BookmarksManager.Migrations.Sqlite \
  --startup-project src/BookmarksManager.Api

EF_PROVIDER=MariaDb dotnet ef migrations add InitialCreate \
  --project src/BookmarksManager.Migrations.MariaDb \
  --startup-project src/BookmarksManager.Api