dpz.core — Agent Guide

Project structure

  • .NET 10 monorepo, ~30 projects under src/
  • Solution file: src/Dpz.Core.slnx (modern .slnx format — pass it explicitly to dotnet)
  • src/ is the working root for all dotnet/tool commands (see CSharpier note below)
  • Sensitive secrets live in PRIVATE.md at the repo root — never commit changes to it, never echo its contents
  • Test project Dzp.Core.XunitBasic has an intentional typo ("Dzp" not "Dpz") — do not rename it

Quick commands

# Build (run from src/ or pass full path)
dotnet build ./Dpz.Core.slnx        # from src/
dotnet build src/Dpz.Core.slnx      # from repo root

# Test all / single project
dotnet test src/Dpz.Core.slnx
dotnet test src/Dpz.Core.Backup.Test/Dpz.Core.Backup.Test.csproj

# Format C# — tool manifest is at src/dotnet-tools.json (NOT standard .config/)
# These MUST be run from src/, not the repo root
dotnet tool restore     # in src/
dotnet csharpier .      # in src/  (config: src/.csharpierrc.yaml, csharpier 1.2.5)

# Frontend dev — run from src/Dpz.Core.Web/
.\build.ps1 init      # one-time: npm install + verify config + smoke-test prod build
.\build.ps1 dev       # watch mode (app module only) — alias for npm run dev:app
.\build.ps1 dev-all   # watch all modules (app + member + ai-chat) via concurrently
.\build.ps1 build     # production bundle (CSS + JS + hash + manifest)
# clean-css-cli is a local devDependency (resolved from node_modules/.bin); no global install required

# Frontend lint/format (run from src/Dpz.Core.Web/ after `npm install`)
npm run lint
npm run format
npm run format:check

Architecture essentials

LayerProjectRole
WebDpz.Core.WebMain MVC app (container port 2372)
AuthDpz.Core.AuthSSO / OpenIddict 7.5.0 (container port 2377, no GitHub Actions deploy)
APIDpz.Core.WebApiREST API (container port 2376)
JobsDpz.Core.Web.JobsHangfire background tasks — runs as systemd service, NOT Docker
DBDpz.Core.MongodbAccessRepository + Unit of Work pattern
ConfigDpz.Core.InfrastructureShared utilities, rate limiting, middleware
MediatorDpz.Core.Service.MediatorCQRS request handlers (martinothamar Mediator, NOT MediatR)
MessagesDpz.Core.MessageQueueRabbitMQ publisher/consumer + Outbox pattern
ModelsDpz.Core.Public.Entity / Dpz.Core.Public.ViewModelEntities (DB-only) / DTOs (public API)

Entrypoints: Dpz.Core.Web/Program.cs, Dpz.Core.WebApi/Program.cs, Dpz.Core.Auth/Program.cs, Dpz.Core.Web.Jobs/Program.cs. Local dotnet run uses each project's launchSettings.json (NOT the container ports above).

Microsoft / OpenIddict package versions are centralized in src/Directory.Build.props — bump versions there, not per-csproj.

Coding conventions

  • Indent: 4 spaces. Max line: 100 chars.
  • File-scoped namespaces: namespace X.Y;
  • Private fields: _camelCase prefix
  • Braces always required (even single-line if/for/etc.)
  • No trailing inline comments (var x = 1; // bad) — convention is review-only, no Roslyn analyzer enforces it
  • No public fields — use properties
  • Primary constructors when only one constructor
  • Async methods must end in Async, always accept CancellationToken cancellationToken = default
  • Structured logging only — no string interpolation in Serilog calls
  • Return empty collections, never null, for arrays/lists (byte[]? is ok)
  • Parameters: as abstract as possible. Returns: as concrete as possible.
  • Uses C# 14 extension method syntax (not traditional this parameter)
  • Service public methods (and interface signatures) must never return entity types directly. Entities live in Dpz.Core.Public.Entity and are used only inside repositories/services; the public API of a service must return DTO / ViewModel / Response types from Dpz.Core.Public.ViewModel (e.g. VmVideo, MusicResponse, CommentViewModel). This isolates the persistence model from callers.
  • Object mapping uses Mapster (IMapper / TypeAdapterConfig), not AutoMapper.

Service DI registration (config-driven via RegisterInject)

Dpz.Core.Service/ServiceDependencyInjection.cs exposes AddDefaultServices(IConfiguration), called from Program.cs of Web, WebApi, Auth, and Web.Jobs. There is no [Service]/attribute scan and no hand-written services.AddScoped<IFoo, Foo>() for domain services.

Bindings come from a RegisterInject config section read via standard IConfiguration. In this repo the team publishes it through AgileConfig for convenience (it is intentionally absent from every local appsettings*.json), but any config source works — adding it to appsettings.Development.json is valid for local debugging. Grepping the repo for service registrations will find nothing; check AgileConfig (or whichever source you've added it to).

Schema (each list item):

  • InterfaceAssemblyName + InterfaceNamespace — scan all interfaces in this namespace
  • ImplementAssemblyName + ImplementNamespace — first non-abstract type assignable to each interface is auto-registered as Scoped
  • Remove: [<interface FQN>, ...] — skip these interfaces from auto-registration
  • Add: [{ Type, ServiceFullName, ImplementFullName }] — explicit override; Type is Transient | Singleton | Scoped (default)

Current production config (for reference — schema, not values to copy blindly):

"RegisterInject": [
  {
    "InterfaceAssemblyName": "Dpz.Core.Service",
    "InterfaceNamespace":   "Dpz.Core.Service.RepositoryService",
    "ImplementAssemblyName": "Dpz.Core.Service",
    "ImplementNamespace":   "Dpz.Core.Service.RepositoryServiceImpl",
    "Remove": ["Dpz.Core.Service.RepositoryService.ISteamGameService"]
  }
]

ISteamGameService is excluded from auto-registration because it is wired via AddHttpClient<ISteamGameService, SteamGameService>() manually in each consuming project (Web.Jobs, WebApi, ServiceTest). The V4 namespace pair (Dpz.Core.Service.V4.*) is mentioned in older docs but the src/Dpz.Core.Service/V4/ directory does not exist locally — it is configured via AgileConfig only if active.

Implications when adding a new service:

  • Drop the interface in a scanned interface namespace and its impl in the matching impl namespace — DI is automatic, no code change needed
  • To deviate from Scoped, register a second impl, or exclude one, edit the RegisterInject config (Add / Remove) — not the codebase
  • Putting the impl outside the configured ImplementNamespace will silently leave the service unregistered
  • Only the first assignable impl in ImplementNamespace wins for auto-registration; if you have multiple, use Add to be explicit

Mediator (martinothamar Mediator, NOT MediatR)

See .agent/skills/mediator/SKILL.md for full details. Key points:

  • Package: Mediator by martinothamar v3.0.2 — source-generator based. Do NOT hand-register handlers; the generator wires them automatically.
  • Registered via AddDpzMediator() called inside AddBusinessServices().
  • Pipeline: MediatorLoggingBehaviorMediatorPerformanceBehavior (≥800 ms warning) → MediatorExceptionWrappingBehavior
  • Handlers inject: IRepository<>, IFusionCache, IMessagePublisher<>, IMapper, IMediator, IHttpClientFactory, IConfiguration, ILogger<> — never business service interfaces from Dpz.Core.Service
  • Return types: ResponseResult/ResponseResult<T> for API-boundary requests; DTO/ViewModel/nullable for queries; never Public.Entity.*
  • Feature modules live in src/Dpz.Core.Service.Mediator/Features/: Article, Code, Health, Markdown, Media, Search, Storage, Video
  • Dependency direction: MongodbAccess → Mediator → Service → Application

Messaging (RabbitMQ + Outbox)

  • All message types inherit MessageBase (from Dpz.Core.Entity.Base) and live in Dpz.Core.Public.ViewModel.Messages/
  • Routing auto-derived from class name: e.g. NewsArticleMessage → exchange dpz.news.exchange, queue dpz.news.article.queue. Override with [MessageRoute].
  • AddRabbitMQ(configuration) is called inside AddBusinessServices() — all apps get the publisher automatically
  • Register consumers per-app with AddMessageConsumer<TMsg, THandler>()
  • Consumers: ClearCacheMessage (Web), NewsArticleMessage+BatchCompletionMessage (WebApi), several email/media messages (Web.Jobs)
  • Outbox (AddMessageOutbox()) provides MongoDB-backed reliable delivery; retry jobs run every minute in Web.Jobs
  • Config section: "RabbitMQ" with HostName, Port, UserName, Password, VirtualHost

Caching (FusionCache)

  • Primary cache abstraction: ZiggyCreatures.FusionCache (L1 memory + L2 Redis + Redis backplane)
  • Registered in AddBusinessServices() — inject IFusionCache directly
  • Services that cache inherit AbstractCacheService which wraps GetOrSetAsync with tag-based invalidation (RemoveByTagAsync)
  • Cache prefix key defaults to Type.FullName; override per service
  • In dev: distributed lock via FileDistributedSynchronizationProvider (config key FileLockPath, default D:\backup\dpz.core.lock); production uses RedisDistributedSynchronizationProvider
  • IP rate limiting also uses FusionCache+Redis (cache key: "RateLimit:IP:{IP}"); middleware order: UseIpRateLimit() before UseRejectBots()

Testing quirks

  • xUnit + Microsoft.NET.Test.Sdk
  • Integration tests (Backup.Test, ServiceTest, etc.) require MongoDB running; rely on appsettings.Test.json
  • MongoDB single instance does NOT support transactions — must use replica set (--replSet rs0)
  • Tests that use IUnitOfWork (transactions) only pass with MongoDB replica set
  • CI (.github/workflows/build.yml) only runs dotnet restore + dotnet buildno tests run in CI yet

Config & prerequisites

Required at runtime (throw InvalidConfigurationException if absent in Web; checked in respective Program.cs):

  • AgileConfig:appId, AgileConfig:secret, AgileConfig:nodes — central config server; valid AgileConfig:env values: DEV, TEST, STAGING, PROD
  • ConnectionStrings:mongodb — main MongoDB
  • ConnectionStrings:hangfireMongodbseparate connection string used exclusively for Hangfire storage
  • ConnectionStrings:redis — Redis (SignalR backplane, FusionCache L2, distributed locks)
  • LibraryHost, AssetsHost — required by Dpz.Core.Web
  • Server:Issuer — required by Dpz.Core.WebApi (JWT authority URL, points to Auth server)
  • WebApiHangfireCollectionPrefix — required by Dpz.Core.WebApi; STAGING uses "STAGING_WebAPI" to avoid MongoDB collection collision
  • hangfireStatus:ItHomeDelete|ItHomeUpdate|SteamUpdate|Backup — boolean flags to enable/disable recurring Web.Jobs jobs
  • RabbitMQ:HostName|Port|UserName|Password|VirtualHost

NuGet feed: GitHub Packages at https://nuget.pkg.github.com/pengqian089/index.json (credentials embedded in src/NuGet.config)

Frontend (Dpz.Core.Web)

  • TypeScript + esbuild 0.27.1 (pinned, not webpack/vite). ESM modules. esbuild configs in src/Dpz.Core.Web/esbuild/ (NOT under wwwroot/)
  • Source TS lives in src/Dpz.Core.Web/wwwroot/scripts/
  • CSS follows BEM naming. Shared partials prefixed _ (e.g. _pagination.css)
  • Three entry bundles: app, member, chat (internally ai-chat) — built separately via npm run build:app|build:member|build:chat
  • Dev mode outputs plain .dev.js; production adds content-hash filenames (8-char base32 for JS, 8-char lowercase hex for CSS)
  • Partial manifests: esbuild/.app.manifest.json, esbuild/.member.manifest.json, esbuild/.chat.manifest.json → merged to wwwroot/assets-manifest.json
  • Production CSS pipeline uses clean-css-cli (local devDependency, resolved from node_modules/.bin/cleancss by build.ps1) to merge _IncludeCssFilePartial.cshtml references into global.min.css
  • Pre-built frontend assets are committed to the repo; no frontend build step in CI or Docker builds
  • jQuery + SignalR client for real-time features; supports MessagePack (application/x-msgpack) in WebApi

Deployment

All deploys are manual via GitHub Actions workflow_dispatch:

  • deploy-core.yml — Docker build+deploy Dpz.Core.Web to two servers (SERVER_1 and SERVER_2)
  • deploy-api.yml — Docker build+deploy Dpz.Core.WebApi to two servers (SERVER_1 and SERVER_2)
  • deploy-job.ymldotnet publish locally, rsync to HONGKONG_SERVER, restarts dpz-job.service via systemd (NOT Docker)
  • STAGING-WEBAPI.yml — Docker staging WebApi on SERVER_1 only (port 3509, AgileConfig:env=STAGING)
  • No GitHub Actions workflow exists for Dpz.Core.Auth — deploy is manual or via a separate pipeline

Docker build pattern (from src/ context):

cd src
docker build -t dpz.core -f Dpz.Core.Web/Dockerfile .
docker run --restart=always --name dpz.core -e TZ=Asia/Shanghai -p 2372:8080 -d dpz.core:latest

Use -e "AgileConfig:env=STAGING" for non-Prod environments.

Notable paths

  • Hangfire dashboard — Web: /runtask (constant Program.HangfireDashboardPath); Web.Jobs: /jobs (constant Program.ConsoleUrl); WebApi: /jobs in DEBUG only
  • Auth cookie name — Web: Dpz.Web.Core.Authoriza; WebApi: Dpz.Web.Api.Server.Authoriza
  • API docs (Scalar): /scalar/v1 (theme: Saturn)
  • Rate-limit policy name: "comment" (3 req/min, FixedWindow)
  • EnumLibrary can be published as NuGet: dotnet pack -c Release then dotnet nuget push

Branch naming

<type>/<issue-id>-<short-description> — types: feature/, bugfix//fix/, hotfix/, release/, chore/, docs/, refactor/, test/

What CI does (and doesn't)

.github/workflows/build.yml (push/PR to master):

  • dotnet restoredotnet buildno lint, no format check, no tests
  • Code formatting must be enforced locally: dotnet csharpier . (in src/) + npm run format:check (in src/Dpz.Core.Web/)
评论加载中...