Files
RpgRoller/TASKS.md

23 KiB

Refactor Blueprint: GameService and Workspace

Purpose

This document is the implementation blueprint for splitting two oversized classes without changing product behavior:

  • RpgRoller/Services/GameService.cs
  • RpgRoller/Components/Pages/Workspace.razor.cs

The goal is to reduce future churn, make responsibilities explicit, improve testability, and create stable seams for future features.

This is a planning artifact only. It does not authorize behavior changes. During implementation, preserve the existing public API contracts and current user-visible behavior unless a separate task explicitly requests functional changes.

Primary Constraints

  • Do not use partial classes as the main decomposition strategy.
  • Prefer composition of small sealed classes with clear ownership.
  • Keep IGameService stable during the refactor unless a separate change requests an API redesign.
  • Preserve current endpoint contracts and WorkspaceQueryService behavior.
  • Preserve current workspace UX and screen flow.
  • Do not revert unrelated local changes already present in the repo.
  • Keep changes incremental and validation-heavy.

Desired End State

Backend

  • GameService becomes a thin façade/coordinator.
  • Stateful concerns are centralized in a shared state owner instead of spread across one giant class.
  • Domain workflows are split into small sealed collaborators.
  • Pure logic helpers move into small focused files.
  • File boundaries reflect change frequency and business ownership.

Frontend

  • Workspace becomes a thin Blazor component coordinator.
  • State, orchestration, admin actions, play/log actions, and live event synchronization live in separate sealed collaborators.
  • Razor bindings reference composed state/actions objects instead of one monolithic code-behind surface.
  • Existing child component boundaries remain intact unless a very small binding adjustment is needed.

Current Problems To Solve

GameService

Observed concerns mixed together today:

  • authentication and session lifecycle
  • admin user management
  • campaign lifecycle
  • character lifecycle and ownership transfer
  • skill and skill-group lifecycle
  • dice validation and roll execution
  • campaign log shaping and roll-detail formatting
  • DTO mapping
  • in-memory state tracking
  • persistence load/save/cloning

This creates the wrong coupling pattern:

  • changing one workflow risks merge conflicts with unrelated work
  • high-level service methods depend on low-level helper implementation details
  • lock/state/persistence concerns are interleaved with domain logic
  • roll engine logic is too close to CRUD flows
  • pure helpers are harder to discover and test in isolation

Workspace

Observed concerns mixed together today:

  • initial bootstrap
  • session reload/logout
  • screen selection and persistence
  • campaign selection and scope refresh
  • management actions
  • play-screen actions
  • campaign log detail caching
  • admin actions
  • live state event synchronization
  • toast/status handling
  • UI state storage and computed properties

This creates the wrong coupling pattern:

  • every change lands in one file
  • state mutation rules are difficult to audit
  • JS interop ownership is spread across unrelated methods
  • event-driven refresh logic is coupled to screen and toast logic
  • the Razor file binds against too many members on one type

Architecture Direction

1. Backend Composition Strategy

1.1 Keep a thin GameService façade

GameService should stay as the application-facing implementation of IGameService, but it should stop owning every workflow directly.

Target shape:

  • constructor wires shared collaborators
  • each public IGameService method delegates to one domain-oriented sealed service
  • the façade owns no domain-heavy private logic
  • only lightweight delegation and composition remain in GameService

This preserves integration points while still removing the monolith.

1.2 Introduce a shared state owner

Create a single shared state owner to encapsulate the in-memory model, synchronization gate, and persistence coordination used by the composed backend services.

Candidate responsibility set:

  • lock object
  • dictionaries/lists currently stored in GameService
  • lookup helpers for common state access
  • campaign-state tracker storage
  • database load/save orchestration hooks

Recommended candidate names:

  • GameStateStore
  • GameRuntimeState
  • GameStateCoordinator

Preferred choice: GameStateStore

Why:

  • "Store" matches the fact that it owns mutable in-memory state
  • it avoids implying EF persistence semantics
  • it gives the rest of the services a stable dependency name

This shared state owner must be the only place that exposes the mutable collections and the synchronization gate needed by the domain services.

1.3 Split backend workflows into domain services

Create sealed services grouped by real business seams, not arbitrary line-count slices.

Recommended services:

  1. GameAuthService
  2. GameUserAdministrationService
  3. GameCampaignService
  4. GameCharacterService
  5. GameSkillService
  6. GameRollService
  7. GamePersistenceService

Each service should be small enough to own one family of change, but large enough that one feature does not have to hop across many tiny orchestration layers.

1.4 Extract pure helper classes into separate files

Pure or near-pure helpers should not stay hidden inside the coordinator or domain services.

Recommended helper extraction:

  • SkillDefinitionValidator
  • RollVisibilityParser
  • CustomRollOptionsResolver
  • RollBreakdownFormatter
  • CampaignLogSummaryBuilder
  • GameDtoMapper
  • GameStateCloneFactory
  • RoleSerializer

If a helper remains too broad after extraction, split again. Small helper files are desirable here because they are stable, discoverable, and easy to test.

2. Backend Responsibility Map

2.1 GameService

GameService should delegate the following methods:

  • auth/session
    • Register
    • Login
    • Logout
    • GetUserBySession
    • GetMe
  • campaigns
    • CreateCampaign
    • GetCampaigns
    • GetCharacterCampaignOptions
    • GetCampaign
    • DeleteCampaign
  • users/admin
    • GetUsernames
    • GetUsers
    • UpdateUserRoles
    • DeleteUser
  • characters
    • CreateCharacter
    • UpdateCharacter
    • DeleteCharacter
    • ActivateCharacter
    • GetOwnCharacters
  • skills
    • CreateSkillGroup
    • UpdateSkillGroup
    • DeleteSkillGroup
    • CreateSkill
    • UpdateSkill
    • DeleteSkill
    • GetCharacterSheet
  • rolls/log
    • RollSkill
    • RollCustom
    • GetCampaignLog
    • GetCampaignLogPage
    • GetRollDetail
    • GetCampaignStateSnapshot
  • rulesets
    • GetRulesets

2.2 GameAuthService

Own:

  • register/login/logout/session lookup
  • me-response shaping tied to session and active-character resolution
  • username normalization usage in auth flows
  • session creation

Dependencies:

  • GameStateStore
  • IPasswordHasher<UserAccount>
  • GamePersistenceService
  • GameDtoMapper

2.3 GameUserAdministrationService

Own:

  • GetUsernames
  • GetUsers
  • UpdateUserRoles
  • DeleteUser
  • role checks used by admin flows

Dependencies:

  • GameStateStore
  • GamePersistenceService
  • GameDtoMapper
  • RoleSerializer
  • common authorization helpers

2.4 GameCampaignService

Own:

  • create/list/get/delete campaign flows
  • campaign visibility rules
  • campaign context resolution for viewable campaigns
  • campaign roster shaping

Dependencies:

  • GameStateStore
  • GamePersistenceService
  • GameDtoMapper
  • shared authorization/context helpers

2.5 GameCharacterService

Own:

  • create/update/delete/activate character flows
  • owner transfer rules
  • active-character consistency rules
  • own-character listing
  • character-to-campaign state tracker updates

Dependencies:

  • GameStateStore
  • GamePersistenceService
  • GameDtoMapper
  • shared authorization/context helpers

2.6 GameSkillService

Own:

  • skill-group CRUD
  • skill CRUD
  • character sheet read model
  • skill-group assignment rules
  • skill definition validation orchestration

Dependencies:

  • GameStateStore
  • GamePersistenceService
  • GameDtoMapper
  • SkillDefinitionValidator
  • shared authorization/context helpers

2.7 GameRollService

Own:

  • RollSkill
  • RollCustom
  • campaign log reads
  • log page reads
  • roll detail reads
  • campaign state snapshot reads
  • roll recording
  • log visibility checks
  • compact log summary generation

Dependencies:

  • GameStateStore
  • GamePersistenceService
  • GameDtoMapper
  • SkillDefinitionValidator only if needed for custom roll flows
  • RollVisibilityParser
  • RollEngine
  • RollBreakdownFormatter
  • CampaignLogSummaryBuilder

2.8 GamePersistenceService

Own:

  • load state from database at startup
  • persist current runtime state
  • clone/snapshot helpers used for persistence boundaries

Dependencies:

  • IDbContextFactory<RpgRollerDbContext>
  • GameStateStore
  • GameStateCloneFactory

Implementation note:

  • keep EF persistence concerns here
  • do not let other domain services talk directly to EF unless there is a deliberate redesign later

3. Backend Cross-Cutting Helpers

Some logic is shared across multiple domain services but should still remain explicit instead of hidden in the state store.

Recommended small helper files:

Authorization and context

  • GameAuthorization

    • user role check helpers
    • can-view campaign
    • can-edit character
    • can-view roll
  • GameContextResolver

    • resolve user from session
    • resolve campaign context
    • resolve character campaign
    • resolve current campaign id

These can be static helper classes if they remain pure over store inputs, or sealed services if constructor-injected collaborators make the code cleaner. Prefer the simpler shape once implementation starts.

Mapping

  • GameDtoMapper
    • ToUserSummary
    • ToAdminUserSummary
    • ToCampaignOption
    • ToCampaignSummary
    • ToCampaignRoster
    • ToCharacterSheet
    • ToCharacterSummary
    • ToCampaignStateSnapshot
    • ToSkillGroupSummary
    • ToSkillSummary
    • ToRollResult
    • ToLogEntry
    • ToLogListEntry

Persistence and clone helpers

  • GameStateCloneFactory

    • clone user/session/campaign/character/skill/skill-group/roll-log entry
  • RoleSerializer

    • parse/serialize/normalize roles

Roll engine helpers

  • SkillDefinitionValidator

    • expression parse/option validation
  • RollEngine

    • top-level roll dispatch by ruleset
  • StandardRollEngine

  • D6RollEngine

  • RolemasterRollEngine

  • RollBreakdownFormatter

    • shared textual breakdown formatting
  • CampaignLogSummaryBuilder

    • compact summary + badges + expression extraction helpers

The roll-engine area is a strong candidate for several very small files because the behavior is algorithmic and changes independently from CRUD flows.

One reasonable target layout:

RpgRoller/Services/
  GameService.cs
  GameStateStore.cs
  GamePersistenceService.cs
  GameAuthService.cs
  GameUserAdministrationService.cs
  GameCampaignService.cs
  GameCharacterService.cs
  GameSkillService.cs
  GameRollService.cs
  GameAuthorization.cs
  GameContextResolver.cs
  GameDtoMapper.cs
  GameStateCloneFactory.cs
  RoleSerializer.cs
  SkillDefinitionValidator.cs
  RollVisibilityParser.cs
  CustomRollOptionsResolver.cs
  RollEngine.cs
  StandardRollEngine.cs
  D6RollEngine.cs
  RolemasterRollEngine.cs
  RollBreakdownFormatter.cs
  CampaignLogSummaryBuilder.cs

Exact filenames can be adjusted during implementation if a nearby convention in the repo suggests better naming. The important part is the separation of responsibilities, not the precise suffix.

5. Frontend Composition Strategy

5.1 Keep Workspace as the component boundary

Workspace should remain the Blazor component type used by Workspace.razor, but it should stop directly owning all workflow code.

Target shape:

  • Workspace keeps injected dependencies and lifecycle entry points
  • composed sealed collaborators handle most behavior
  • Workspace exposes only the state/actions needed by the Razor file

This avoids breaking the component boundary while still removing the monolith.

5.2 Introduce a dedicated state holder

Create a single state holder for workspace UI state and view-model data.

Recommended candidate name:

  • WorkspaceState

Own:

  • authenticated user/session-derived state
  • selected campaign and character state
  • campaign collections
  • rule set collections
  • admin user collection
  • log detail cache
  • toast state
  • screen/mobile-panel state
  • live connection state
  • modal state
  • computed helpers that are pure projections over stored state

This state holder should be easy to inspect and should reduce the risk of hidden cross-method mutations.

5.3 Split workspace orchestration into sealed collaborators

Recommended services:

  1. WorkspaceSessionCoordinator
  2. WorkspaceCampaignCoordinator
  3. WorkspacePlayCoordinator
  4. WorkspaceAdminCoordinator
  5. WorkspaceLiveStateController
  6. WorkspaceFeedbackService

These names can be adjusted, but the responsibilities should stay distinct.

6. Frontend Responsibility Map

6.1 Workspace component

Own:

  • injected dependencies
  • collaborator construction/wiring
  • lifecycle delegation
  • JS-invokable entry points delegating to collaborators
  • final binding surface consumed by Razor

Keep thin. Do not let it become a second coordinator monolith with only renamed methods.

6.2 WorkspaceState

Own:

  • scalar UI state
  • collections
  • selected entities
  • modal visibility and form bootstrap models
  • log detail cache and loading/error maps
  • toast list
  • computed view properties

Recommended computed properties to live here if kept pure:

  • selected campaign name
  • selected character
  • play-selected campaign
  • play-selected character
  • play-selected character id
  • filtered play skill and skill-group views if still direct projections
  • current-user role flags
  • campaign delete permission
  • screen flags
  • connection-state label/css
  • app CSS class

6.3 WorkspaceSessionCoordinator

Own:

  • initial bootstrap sequence
  • session reload
  • logout flow
  • health retry flow
  • persisted session-storage reads/writes for screen, panel, campaign, roll visibility
  • clearing authenticated state

Dependencies:

  • WorkspaceState
  • WorkspaceQueryService
  • RpgRollerApiClient
  • IJSRuntime
  • WorkspaceLiveStateController
  • WorkspaceFeedbackService

6.4 WorkspaceCampaignCoordinator

Own:

  • campaign reload and selection
  • campaign scope refresh
  • campaign roster refresh
  • character campaign options reload
  • management-screen campaign and character mutations
  • character modal open/close and bootstrap
  • selected-character synchronization

Dependencies:

  • WorkspaceState
  • WorkspaceQueryService
  • RpgRollerApiClient
  • IJSRuntime
  • WorkspaceLiveStateController
  • WorkspaceFeedbackService

6.5 WorkspacePlayCoordinator

Own:

  • selected character activation
  • selected character sheet refresh
  • skill roll submission
  • custom roll handling
  • campaign log page refresh
  • roll detail expansion/loading/cache trimming
  • roll visibility changes
  • play-panel errors

Dependencies:

  • WorkspaceState
  • WorkspaceQueryService
  • RpgRollerApiClient
  • IJSRuntime
  • WorkspaceFeedbackService

6.6 WorkspaceAdminCoordinator

Own:

  • admin screen access enforcement
  • admin user load
  • toggle admin role
  • delete user

Dependencies:

  • WorkspaceState
  • WorkspaceQueryService
  • RpgRollerApiClient
  • IJSRuntime
  • WorkspaceLiveStateController
  • WorkspaceFeedbackService

6.7 WorkspaceLiveStateController

Own:

  • start/stop SSE state events
  • JS invokable state snapshot handling
  • JS invokable connection-state updates
  • current campaign-state refresh logic
  • campaign-state tracking reset logic

Dependencies:

  • WorkspaceState
  • WorkspaceCampaignCoordinator
  • WorkspacePlayCoordinator
  • IJSRuntime

Critical rule:

  • this class should own the live-update reconciliation logic
  • do not let campaign reload logic and live event logic drift into different locations again

6.8 WorkspaceFeedbackService

Own:

  • status-to-toast behavior
  • live announcement updates
  • delayed toast dismissal

Dependencies:

  • WorkspaceState
  • component refresh callback or dispatcher mechanism

Implementation note:

  • if callback wiring becomes awkward, this service can remain a helper owned by Workspace rather than a DI service
  • the important part is isolating feedback behavior from campaign/admin/play workflows

7. Frontend Binding Strategy

If composition is used, Workspace.razor bindings will need to reference state and actions through the new composed surface.

Example direction:

  • state data from State
  • management actions from Campaigns
  • play actions from Play
  • admin actions from Admin
  • session/bootstrap actions from Session

Illustrative pattern only:

<AppHeader
    User="State.User"
    CampaignName="@State.SelectedCampaignName"
    LogoutRequested="Session.LogoutAsync" />

Another example:

<CampaignManagementPanel
    Campaigns="State.Campaigns"
    SelectedCampaignId="State.SelectedCampaignId"
    CampaignSelectionChanged="Campaigns.OnCampaignSelectionChangedAsync"
    DeleteCampaignRequested="Campaigns.DeleteSelectedCampaignAsync" />

Important guardrail:

  • avoid replacing one huge component surface with many deeply chained bindings that are hard to read
  • prefer a small number of clearly named composed properties on Workspace

For example:

  • State
  • Session
  • Campaigns
  • Play
  • Admin
  • Live

One reasonable target layout:

RpgRoller/Components/Pages/
  Workspace.razor
  Workspace.razor.cs
  WorkspaceState.cs
  WorkspaceSessionCoordinator.cs
  WorkspaceCampaignCoordinator.cs
  WorkspacePlayCoordinator.cs
  WorkspaceAdminCoordinator.cs
  WorkspaceLiveStateController.cs
  WorkspaceFeedbackService.cs
  WorkspaceToast.cs

If some files become too small, merge only where the change patterns are clearly the same. Do not merge simply to reduce file count.

9. Implementation Order

The implementation phase should proceed in small safe steps.

Phase 1: Backend enabling extractions

  1. Extract pure helper classes from GameService first.
  2. Keep GameService behavior identical while moving algorithmic helpers out.
  3. Add or update tests around the extracted helpers if coverage or confidence drops.

Why first:

  • lowest integration risk
  • creates stable dependencies for later service extraction

Phase 2: Introduce GameStateStore and GamePersistenceService

  1. Move runtime collections, gate, and persistence primitives behind the shared state owner.
  2. Move database load/save logic into persistence service.
  3. Keep GameService still acting as the main orchestrator while the shared dependencies settle.

Why second:

  • makes later service extraction mechanical instead of risky

Phase 3: Extract backend domain services

Suggested order:

  1. auth
  2. campaigns
  3. characters
  4. skills
  5. rolls/log
  6. admin/users

This order keeps the highest-risk workflow area, rolls/logs, until the shared helpers and state seams are already stable.

Phase 4: Thin GameService façade

  1. Replace remaining logic in GameService with delegation only.
  2. Confirm constructor wiring stays readable.
  3. Re-run tests and local CI.

Phase 5: Frontend state extraction

  1. Introduce WorkspaceState.
  2. Move raw state and pure computed projections first.
  3. Keep method behavior temporarily in Workspace until state references are stabilized.

Why first:

  • it shrinks the cognitive load before moving orchestration

Phase 6: Frontend coordinator extraction

Suggested order:

  1. feedback/toasts
  2. session/bootstrap
  3. campaign management
  4. play/log
  5. admin
  6. live-state controller

This order reduces the chance that live event logic is extracted before its dependent refresh flows are already stable.

Phase 7: Razor binding cleanup

  1. Update bindings to the new composed surface.
  2. Keep markup structure stable.
  3. Avoid stylistic churn unrelated to composition.

Phase 8: Documentation and verification

  1. Update README.md code-organization notes if file layout changed materially.
  2. Run local CI.
  3. Run Playwright smoke flow.
  4. Re-check coverage expectations.

10. Testing and Validation Expectations

During implementation, after each meaningful iteration:

  • run pwsh ./scripts/ci-local.ps1
  • verify test coverage remains acceptable relative to the repo rules
  • add tests where extraction creates untested seams
  • after frontend-affecting iterations, run the Playwright smoke flow

Specific test focus areas:

Backend

  • auth/session regression
  • campaign visibility rules
  • character ownership transfer
  • skill-group assignment and validation
  • D6 roll behavior
  • Rolemaster roll behavior
  • custom roll behavior
  • log paging and detail visibility
  • persistence reload behavior
  • admin role mutation and user deletion side effects

Frontend

  • workspace bootstrap still restores screen/campaign/panel/visibility preferences
  • admin screen still enforces role access
  • play screen still refreshes log/roster/sheet correctly
  • newly recorded roll still appears and auto-expands as before
  • live connection state still updates announcements and fallback state

11. Risks and Guardrails

Risk: Hidden behavior drift during backend extraction

Guardrail:

  • keep IGameService contract fixed
  • extract pure helpers before orchestration changes
  • preserve method-level tests and add new coverage around moved logic

Risk: Locking and persistence bugs after splitting GameService

Guardrail:

  • centralize mutable runtime state in one store
  • centralize persistence writes in one service
  • do not let each domain service invent its own lock pattern

Risk: Workspace composition creates binding sprawl

Guardrail:

  • expose a small composed surface from Workspace
  • keep naming direct and role-based
  • avoid deep object chains in Razor

Risk: Live-state refresh logic breaks after movement

Guardrail:

  • move refresh logic together with live event handling ownership
  • keep campaign roster, selected sheet, and log refresh paths explicit
  • verify with Playwright after frontend extraction

Risk: Over-fragmentation

Guardrail:

  • many small helper files are good for pure logic
  • domain orchestration services should remain coarse enough to own meaningful workflows
  • do not create tiny services that only forward one call with no real ownership

12. Non-Goals

The implementation phase described by this blueprint should not, by default:

  • redesign API contracts
  • redesign the workspace UX
  • replace Blazor patterns with a different UI architecture
  • change persistence strategy away from the current runtime-state-plus-SQLite model
  • introduce unrelated feature work

13. Definition of Done For The Refactor

The refactor is complete when all of the following are true:

  • GameService is a thin coordinator, not a monolith
  • backend workflows are owned by composed sealed classes with clear responsibility boundaries
  • algorithmic and mapping helpers live in small focused files
  • Workspace is a thin component coordinator, not a monolithic code-behind
  • workspace state and workflow orchestration are separated cleanly
  • Workspace.razor binds against a readable composed surface
  • tests still pass
  • local CI still passes
  • frontend smoke verification still passes
  • documentation matches the resulting structure

14. Implementation Reminder

During the actual edit phase:

  • keep changes small
  • validate often
  • do not revert unrelated user changes
  • preserve behavior first
  • optimize for reducing future churn, not merely reducing line count