This file is the canonical coding contract for AI-assisted changes in turbo-cloud.
Tool-specific instruction files should reference this file instead of duplicating rules.
This repository targets the following core stack. When coding, prefer patterns compatible with these versions:
- .NET SDK
9.0.310(fromglobal.json) - C# / BCL
net9.0 - Orleans
9.2.1 - EF Core
9.0.8 - Pomelo MySQL provider
9.0.0 - SuperSocket
2.0.2
Activate the relevant skill checklist before editing code in that domain:
handler-development- Trigger: editing files under
Turbo.PacketHandlers/**or message-to-composer orchestration logic. - Enforce: orchestration-only handlers, no DB queries, canonical grain access, no silent catches.
- Trigger: editing files under
grain-development- Trigger: editing files under
Turbo.*\\Grains\\**orTurbo.Primitives/**/Grains/*.cs. - Enforce: keep ownership boundaries, lifecycle rules, and snapshot/state coherence.
- Enforce: all rules in the Orleans grain development rules section below.
- Trigger: editing files under
session-presence-routing- Trigger: touching session gateway, presence flow, room routing, outbound composer fan-out.
- Enforce: player outbound via
PlayerPresenceGrain.SendComposerAsync; no direct handler socket sends.
message-contracts- Trigger: editing
Turbo.Primitives/Messages/Incoming/**or outgoing composer payload mappings. - Enforce: explicit mandatory fields, no placeholder payloads when source data exists.
- Trigger: editing
revision-protocol- Trigger: changes referencing
Revision<id>packet mappings. - Enforce: edit
Turbo.Revisions/Revision<id>/**inturbo-cloud.
- Trigger: changes referencing
- Build and quality checks in repo files (
Directory.Build.props,Directory.Build.targets,.editorconfig) CONTEXT.mdarchitecture and placement boundaries- Existing neighboring code conventions in the target folder
- Tool-specific adapters (for example
.github/copilot-instructions.md)
Use this request shape with any AI tool:
- Goal:
- Target files:
- Required context files:
- Invariants to preserve:
- Forbidden changes:
- Validation commands:
- Output format:
Default output format:
- concise rationale
- file-by-file diff summary
- risks/assumptions
- exact validation command results
- Target framework/tooling:
.NET 9pinned viaglobal.json. - Keep C# formatting compatible with repo quality gates (
dotnet csharpier check,dotnet format). - Follow
.editorconfignaming/style preferences. - Keep diffs focused and minimal; avoid unrelated refactors.
- Avoid introducing new dependencies unless required by the task.
- Match local conventions in the files you touch.
- Prefer deterministic handlers/services with clear guard clauses.
- Preserve cancellation and async flow where it already exists.
- Handle failure paths explicitly; do not ship happy-path-only changes.
- Avoid dead code, unused allocations, and broad catch blocks that hide errors (see Orleans grain development rules for specifics).
- For revision compatibility work, prefer restoring/adding missing incoming message contracts in
Turbo.Primitives/Messages/Incoming/**before mutating serializer/composer payload behavior. - Do not alter serializer/composer behavior by replacing real payload writes with placeholder constants (for example, unconditional
WriteInteger(0)) unless explicitly requested. - If work references
Revision<id>parsers/serializers, editTurbo.Revisions/Revision<id>/**inturbo-cloud.
These rules exist because every one of these mistakes has shipped and caused real issues.
Every bare catch { } hides a real bug path. Always use catch (Exception ex) and log it.
If a cross-grain notification fails silently, state goes asymmetric and nobody knows why.
- Required: inject
ILogger<T>into every grain that does cross-grain calls or DB work. - Forbidden: bare
catch { },catch (Exception) { }without logging.
When checking status on N grains (e.g. online status for a friend list), do not await each one in a foreach.
Grain calls to different grains can run concurrently with Task.WhenAll.
- Sequential = O(n) round-trips. Parallel = O(1) wall time.
- Apply everywhere: activation hydration, search results, batch accept/deny.
If a grain method calls its own player's GetSummaryAsync inside a loop, hoist the call before the loop.
Same result every iteration = wasted round-trips.
Do not loop ExecuteDeleteAsync per entity. Use a single WHERE ... IN (...) query.
Same for composer fan-out: collect all updates, send once.
Follow the RoomPersistenceGrain pattern: queue dirty state, flush with RegisterGrainTimer on interval, and flush on OnDeactivateAsync.
Do not issue per-event DB writes that block the grain turn.
Handlers already read configuration values (e.g. Turbo:FriendList:UserFriendLimit) from IConfiguration and pass them to grains.
Magic numbers like Take(50), Take(20), or maxIgnoreCapacity = 100 must come from configuration parameters on the grain interface method.
A 10,000 user hotel needs different tuning than a 10 player dev server.
ExecuteDeleteAsync commits immediately and bypasses the EF change tracker.
If a delete + insert must succeed or fail together, use FirstOrDefaultAsync + Remove so both go through one SaveChangesAsync.
Orleans .Ignore() makes cross-grain failures invisible. Use a LogAndForget extension that calls ContinueWith(OnlyOnFaulted) to log the exception.
Still fire-and-forget, but failures are visible in production logs.
Any in-memory collection that grows per-message (e.g. conversation history) must have a configurable cap. Without a cap, long-running sessions leak memory.
Each major domain component should operate in its own grain. When a grain needs heavy I/O (DB writes, persistence flushes), delegate that work to a dedicated secondary grain so it does not block the primary grain's turn.
- Example:
RoomGraindelegates furniture saves toRoomPersistenceGrain. The room grain stays responsive while persistence queues and flushes. - Do not combine domain logic and persistence flushing in the same grain.
Orleans grains are single-threaded by design. Use this for concurrency-sensitive operations by giving each user their own grain for the operation.
- Example: each player gets a
PurchaseGrainso catalog purchases are serialized per-player with no locks needed. - Example: limited-edition items should use a dedicated grain (e.g.
LimitedItemGrain) so concurrent buyers are safely serialized. - Do not add manual locking (
lock,SemaphoreSlim) inside grains — that fights the actor model.
When grain state changes (e.g. wallet balance updates), the grain itself sends the snapshot to PlayerPresenceGrain.SendComposerAsync. The caller that triggered the change does not pass or send the composer — the grain owns that responsibility.
- Correct: handler calls
grain.UpdateWalletAsync(...)→ grain updates state → grain callsPlayerPresenceGrain.SendComposerAsync(...). - Wrong: handler calls
grain.UpdateWalletAsync(...)→ handler builds composer → handler sends composer to player.
Grains may hold cached or in-memory state that will not reflect direct DB changes. All mutations to grain-owned data must go through the grain's methods, even when the player is offline.
- If a grain uses
[PersistentState], state is hydrated from the configured store (not DB) on activation. Direct DB edits will be overwritten by stale store data. - Admin tools and external systems must call grain methods, not issue raw SQL/DB updates, for data that grains own.
- Keep packet handlers orchestration-only:
- validate input
- call grains through canonical grain-factory access patterns
- map snapshot data to outgoing composers
- Do not query database contexts or repositories directly from packet handlers.
- Keep persistence access in grains/services/providers that own domain state.
- Do not use ad-hoc grain key strings in handlers when extension-based access exists.
- Do not add silent
catchblocks in handlers. - When snapshot fields are available, map them into composer payloads instead of TODO placeholders.
- For incoming message records in
Turbo.Primitives/Messages/Incoming/**, keep mandatory fields explicit (userequiredwhere appropriate) instead of default-fallback contracts.
- Connection/session lifecycle starts in gateway flow; do not duplicate session registration in handlers.
- Post-SSO session attachment goes through
PlayerPresenceGrain(one active presence grain per player id). - Player-targeted outbound flow must be:
- resolve player presence grain
- call
SendComposerAsync - rely on presence fan-out to subscribed sessions
- Do not send directly to raw sockets/session transports from packet handlers.
- Active-room membership/discovery belongs to
RoomDirectoryGrain; do not bypass it with ad-hoc room tracking. - Grain lifetime remains Orleans-managed by default; use
[KeepAlive]only for explicitly justified directory/manager grains.
When adding packet mappings in Turbo.Revisions/Revision20260112:
- Update
Turbo.Revisions/Revision20260112/Headers.cs:- add/update incoming
MessageEventid constants - add/update outgoing
MessageComposerid constants
- add/update incoming
- Add parser class under:
Turbo.Revisions/Revision20260112/Parsers/<Domain>/*MessageParser.cs
- Add serializer class under:
Turbo.Revisions/Revision20260112/Serializers/<Domain>/*MessageComposerSerializer.cs
- Register mappings in:
Turbo.Revisions/Revision20260112/Revision20260112.cs- incoming:
Parsersdictionary withMessageEventkey - outgoing:
Serializersdictionary with composer type +MessageComposerid
- Ensure the required
usingdirectives are present inRevision20260112.csfor new parser/serializer namespaces.
- Required context files:
AGENTS.mdCONTEXT.mdTurbo.Primitives/Orleans/GrainFactoryExtensions.cs
- Required references:
- one handler in same domain under
Turbo.PacketHandlers/<Domain>/ - related incoming message type under
Turbo.Primitives/Messages/Incoming/**
- one handler in same domain under
- Forbidden changes:
- no direct DB access in handler
- no direct session/socket sends
- no ad-hoc grain key literals when extension methods exist
- Validation:
dotnet build Turbo.Main/Turbo.Main.csproj -t:TurboCloudFastCheckdotnet build Turbo.Main/Turbo.Main.csproj -t:TurboCloudQualityGate
- Required context files:
AGENTS.mdCONTEXT.md- target grain interface in
Turbo.Primitives/**/Grains/*.cs
- Required references:
- existing grain in same module
- related snapshot/state types in
Turbo.Primitives/Orleans/Snapshots/**orStates/**
- Forbidden changes:
- no handler-layer fallback logic that bypasses grain ownership
- no lifecycle changes that abuse
[KeepAlive]without infrastructure justification
- Validation:
dotnet build Turbo.Main/Turbo.Main.csproj -t:TurboCloudFastCheckdotnet build Turbo.Main/Turbo.Main.csproj -t:TurboCloudQualityGate
- Required context files:
AGENTS.mdCONTEXT.md- neighboring message/composer classes
- Required references:
- incoming message under
Turbo.Primitives/Messages/Incoming/** - outgoing composer under
Turbo.Primitives/Messages/Outgoing/** - handler using same message family
- incoming message under
- Forbidden changes:
- no placeholder payload collections when source snapshot data exists
- no implicit default-fallback contracts for mandatory incoming fields
- Validation:
dotnet build Turbo.Main/Turbo.Main.csproj -t:TurboCloudFastCheckdotnet build Turbo.Main/Turbo.Main.csproj -t:TurboCloudQualityGate
- Required context files:
AGENTS.mdCONTEXT.md- current lookup owner grain/service
- Required references:
- existing set/invalidate methods
- all reverse-lookup call sites
- Forbidden changes:
- no one-way cache updates; forward/reverse mappings must stay coherent
- no loss of case-insensitive semantics for username lookups
- Validation:
dotnet build Turbo.Main/Turbo.Main.csproj -t:TurboCloudFastCheckdotnet build Turbo.Main/Turbo.Main.csproj -t:TurboCloudQualityGate
dotnet build Turbo.Main/Turbo.Main.csproj -t:TurboCloudFastCheck
dotnet build Turbo.Main/Turbo.Main.csproj -t:TurboCloudQualityGate- All modified files match nearby patterns and contract rules.
- Quality gates pass with no new warnings introduced by the change.
- Architecture invariants for touched areas are explicitly confirmed in PR.
- Edge/failure behavior is addressed for logic changes.
- Any context-rule updates needed by the change are included in the same PR.
- Disclose AI usage and major generated sections.
- Be able to explain complex generated logic in your own words.
- Include verification of at least one edge/failure scenario when behavior changes.