Skip to main content

Keystone Diagrams

Architecture Diagram​

This reflects a clean layered architecture:

  • Web: API entry point and HTTP concerns
  • UseCases: application logic and orchestration
  • Core: domain model, aggregates, value objects
  • SharedKernel: common contracts and foundational abstractions
  • Infrastructure: persistence, security, external integrations
  • Tests: validation at unit, integration, functional, and architecture levels

CQRS/DDD Architecture Diagram​

This version reflects the project’s current structure:

  • Keystone.API.Web.csproj is the presentation layer
  • Keystone.API.UseCases.csproj aligns with application-layer command/query handling
  • Keystone.API.Core.csproj represents the DDD domain model
  • Keystone.API.Infrastructure.csproj handles persistence, identity, and integrations
  • Keystone.API.SharedKernel.csproj supports shared contracts and common abstractions

Bounded Context Diagram​

Key DDD alignment notes from the actual code:

  • Aggregate root: User.cs is the single consistency boundary — ExternalLogin and PasswordResetToken are only ever mutated through it (e.g. LinkExternalLogin, RequestPasswordReset), never directly.
  • Value object: UserId strongly types identity instead of a raw Guid.
  • **Invariants enforced in the domain, not application layer — e.g. "cannot remove the only credential" and "unverified external identity can't create a user" are guarded inside User, throwing DomainException.
  • Specifications (UserByEmailSpec, UserByExternalLoginSpec, UserByPasswordResetTokenHashSpec) act as the query-side contract into the aggregate, keeping persistence-ignorant query logic in the domain via Ardalis.Specification.
  • Application layer = context boundary: everything under UseCases/Features/Users (Login, RegisterWithPassword, ExternalLogin, ProvisionAccount, PasswordReset, UnlinkExternalLogin) is the only sanctioned entry point into this bounded context — no other layer touches User directly.
  • Shared Kernel (IAggregateRoot, EntityBase<TId>, IRepository, IUnitOfWork, domain events) is intentionally shared across bounded contexts as common DDD tactical-pattern infrastructure, not context-specific logic.
  • Anti-corruption boundary: Infrastructure (EF Core mapping, KeystoneDbContext, MediatorDomainEventPublisher) translates the pure domain model to/from persistence and external identity providers (OpenIddict) without leaking those concerns into Core.

ER Diagram: Data Model​

Notes on this model, straight from User.cs, ExternalLogin.cs, PasswordResetToken.cs, and UserConfiguration.cs:

  • User is the only aggregate root; ExternalLogin and PasswordResetToken are owned children with no independent lifecycle (OnDelete(Cascade), both constructed via internal factory methods only callable from User).
  • Email on User has a unique index — one account per email.
  • A user can have multiple external logins (one per provider — enforced in code, not the DB) and multiple password reset tokens (history is kept; old ones get marked used rather than deleted).
  • UserId is a value-object wrapper around Guid, converted at the EF layer via HasConversion.

Request Pipeline Flow — Endpoint → Aggregate → Persistence​

Key things this second diagram captures from the real code:

  • Every endpoint under Endpoints/Account/ is a thin adapter — it constructs a command/query and calls mediator.Send, nothing more (verified directly in LoginEndpoint.cs, ProvisionAccountEndpoint.cs, etc.).
  • All state changes route through named behaviors on User (RegisterWithPassword, LinkExternalLogin, RemoveExternalLogin, RequestPasswordReset, ConfirmPasswordReset) — no handler mutates ExternalLogin/PasswordResetToken directly.
  • Query-side reads go through Ardalis Specification classes rather than ad-hoc LINQ, keeping the repository generic.
  • TransactionBehavior (via Mediator pipeline behaviors in Keystone.API.SharedKernel) wraps command handling in a unit of work, and domain events raised on User are dispatched by MediatorDomainEventPublisher after the transaction commits.
  • ExternalLoginCallbackEndpoint/ConfirmLinkEndpoint are the OpenIddict/OIDC federation path; RequestPasswordReset/ConfirmPasswordReset share the same aggregate but a distinct token/security service (Sha256PasswordResetTokenGenerator).

Extended Request Pipeline FLow​

Key details this adds, straight from the code:

RegisterEndpoint vs ProvisionAccountEndpoint — same domain outcome (User.RegisterWithPassword), but deliberately separate front doors per the comments in RegisterEndpoint.cs and ProvisionAccountEndpoint.cs:

RegisterEndpointProvisionAccountEndpoint
RoutePOST /account/registerPOST /v1/accounts/provision
AuthAllowAnonymous()ApiKeyAuthenticationHandler scheme
CallerEnd user via browserCommand Center (trusted service)
HandlerRegisterWithPasswordCommandProvisionAccountCommand
IdempotencyErrors 409 on duplicate emailReturns WasAlreadyRegistered instead of failing
Trust tierPublic, presumably rate-limited/anti-bot candidateExempt from those — single caller acting for many real users

Connect/AuthorizeEndpoint and Connect/TokenEndpoint bypass the UseCases/CQRS layer entirely — they're OpenIddict's designated "passthrough" hooks:

  • AuthorizeEndpoint (GET /connect/authorize) — OpenIddict middleware already validated client_id/redirect_uri/PKCE/scopes before this runs; the endpoint just checks the LoginCookie auth scheme. No cookie → redirect to /account/login (re-entering the normal flow above); valid cookie → builds a ClaimsPrincipal from the subject claim and calls SignInAsync, which OpenIddict turns into an authorization code.
  • TokenEndpoint (POST /connect/token) — handles two grant types: authorization_code (re-signs-in with the principal OpenIddict already persisted against the code) and client_credentials (builds a principal representing the client application itself, via IOpenIddictApplicationManager, with no end user involved at all).
  • Neither endpoint touches User, Repository, or the Mediator pipeline — token/code issuance is entirely OpenIddict's own state machine plus ASP.NET Core's cookie/claims authentication, only crossing into the CQRS/domain layer indirectly via the /account/login redirect when no session cookie exists yet.

Pipeline Flows by Sections​

Caller --> User/Account Endpoints --> OppenIddict​

UseCases (CQRS)​

Domain​

Infrastructure​