Skip to content

refactor(shared): inline UnitOfWork + AxisDbContext per module (ADR-017)#87

Merged
phuongnse merged 2 commits into
mainfrom
refactor/axis-shared-abstractions-only
May 23, 2026
Merged

refactor(shared): inline UnitOfWork + AxisDbContext per module (ADR-017)#87
phuongnse merged 2 commits into
mainfrom
refactor/axis-shared-abstractions-only

Conversation

@phuongnse

@phuongnse phuongnse commented May 23, 2026

Copy link
Copy Markdown
Owner

Summary

Phase 1 PR #4 of the modulith-with-strict-service-boundaries rollout. Narrows Axis.Shared.Infrastructure to abstractions only per ADR-017.

Removed from Shared Replacement
UnitOfWork base class Inlined into 5 module UoW classes (Identity, DataModeling, WorkflowBuilder, WorkflowEngine, FormBuilder)
AxisDbContext base class Inlined OnConfiguring(...) interceptor wiring into 4 module DbContexts (Identity already standalone)
UnitOfWorkTests (base-class test) Coverage transferred to per-module handler/integration tests (466+ of them)
Kept in Shared (genuinely cross-cutting per ADR-017)
TenantSchemaInterceptor — generic, every module needs it identically
HttpTenantContext — implements shared ITenantContext
HandlerLoggingMiddleware — cross-cutting Wolverine middleware
InfrastructureServiceExtensions — DI registration wrapper

Why this scope

Per ADR-017 anti-patterns:

  • "Shared EF Core configuration helpers that secretly require a specific DbContext shape — these belong in module infrastructure" → AxisDbContext base inlined.
  • "A 'BaseRepository' in shared infrastructure — repositories belong in modules" → same logic applies to UnitOfWork base.

Trade-off accepted: ~30 duplicated lines per module's UoW (5 modules × 30 = ~150 net new lines vs ~60 deleted from base). Explicit per-module ownership over hidden inheritance coupling — exactly the intent of ADR-017.

Verification

  • dotnet build 0/0 warnings
  • 466 unit tests pass (all 12 unit-test projects)
  • dotnet format --verify-no-changes clean
  • Testcontainers integration tests deferred to CI

What this PR does NOT do

  • Does not touch per-module DB connection separation (ADR-011) — that's a separate Phase 1 PR.
  • Does not change tenant interceptor behaviour or middleware logic — pure relocation/inlining.
  • Does not affect Identity's DbContext (already standalone — no tenant interceptor since Identity operates in public).

Requirements & rules followed

  • Spec → code — implements ADR-017's "abstractions only" directive
  • Gate 0 — N/A: refactor of existing surface
  • Gate 1 — build + unit tests + format green locally; integration tests deferred to CI
  • Gate 2./scripts/check-doc-drift.sh green; PROGRESS + E03/E04/E05/E06 README updated in same PR
  • Gate 3 — no new durable rule; ADR-017's inline-per-module pattern now applied
  • No new TODO / FIXME / placeholder / stub

Summary by CodeRabbit

  • Refactor

    • Restructured infrastructure to narrow shared abstractions and inline per-module database context and unit-of-work implementations (ADR-017).
  • Documentation

    • Updated progress and epic docs to reflect the narrowed shared kernel and the shift to per-module DB handling and remaining infrastructure work.
  • Tests

    • Removed shared persistence tests; module-specific test coverage to be added separately.

Review Change Stack

Phase 1 step toward "abstractions only" in the shared kernel:

- Delete `Axis.Shared.Infrastructure/Persistence/UnitOfWork.cs` base
  class — inline the same logic (collect events → save → publish) into
  each module's UoW class (Identity, DataModeling, WorkflowBuilder,
  WorkflowEngine, FormBuilder).
- Delete `Axis.Shared.Infrastructure/Persistence/AxisDbContext.cs` base
  class — each module's DbContext now derives from `DbContext` directly
  and inlines the `OnConfiguring` that wires
  `TenantSchemaInterceptor`. The interceptor itself stays in
  Shared.Infrastructure because every module needs it identically.
- Delete the now-orphan `UnitOfWorkTests.cs` in
  `Axis.Shared.Infrastructure.Tests` — the SaveChangesAsync semantics
  are covered by 466+ handler/integration tests in each module.

Per ADR-017's anti-pattern list: "Shared EF Core configuration helpers
that secretly require a specific DbContext shape — these belong in
module infrastructure." Trade-off: ~30 duplicated lines per module's
UoW (5 modules × 30 = ~150 lines net new) for explicit per-module
ownership over hidden inheritance coupling.

What remains in `Axis.Shared.Infrastructure`: `TenantSchemaInterceptor`,
`HttpTenantContext`, `HandlerLoggingMiddleware`, DI extension wrapper.
These are genuinely identical across modules — no module-specific
behaviour to hide.

466 unit tests pass, build 0/0 warnings, format clean.
@coderabbitai

coderabbitai Bot commented May 23, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1b1cca37-ac12-49d7-bd62-5cbbb43fd30a

📥 Commits

Reviewing files that changed from the base of the PR and between baec85b and b348cb2.

📒 Files selected for processing (2)
  • docs/epics/E06-workflow-engine/README.md
  • src/Modules/DataModeling/Axis.DataModeling.Infrastructure/Persistence/DataModelingUnitOfWork.cs
✅ Files skipped from review due to trivial changes (1)
  • docs/epics/E06-workflow-engine/README.md

📝 Walkthrough

Walkthrough

This PR implements ADR-017 by removing shared UnitOfWork and AxisDbContext base classes and inlining their behavior into each module's local DbContext and UnitOfWork: DbContexts register TenantSchemaInterceptor in OnConfiguring, and UnitOfWork implementations explicitly collect/clear domain events, persist with exception translation, and publish events.

Changes

ADR-017: Per-Module Persistence Layer Refactoring

Layer / File(s) Summary
Infrastructure refactoring strategy and scope
docs/PROGRESS.md
Phase 1 progress and Shared Kernel docs updated to record that UnitOfWork and AxisDbContext are now inlined per module per ADR-017 and that shared infrastructure is narrowed to cross-cutting abstractions.
DataModeling module: DbContext and UnitOfWork refactoring
src/Modules/DataModeling/.../DataModelingDbContext.cs, src/Modules/DataModeling/.../DataModelingUnitOfWork.cs
DbContext now derives from DbContext with OnConfiguring registering TenantSchemaInterceptor. UnitOfWork inlined to gather/clear domain events, persist changes, map concurrency and Postgres 23505 unique-constraint errors to domain exceptions, and publish events via Wolverine.
FormBuilder module: DbContext and UnitOfWork refactoring
src/Modules/FormBuilder/.../FormBuilderDbContext.cs, src/Modules/FormBuilder/.../FormBuilderUnitOfWork.cs
FormBuilderDbContext switches to DbContext with tenant interceptor registration in OnConfiguring. FormBuilderUnitOfWork is implemented inline with domain-event collection, clearing, persistence, exception translation, and event publishing.
Identity module: UnitOfWork refactoring
src/Modules/Identity/.../IdentityUnitOfWork.cs
IdentityUnitOfWork refactored from shared-base inheritance to module-local implementation: collects/clears domain events, saves via EF Core, translates concurrency/unique-constraint errors, and publishes events through IMessageBus.
WorkflowBuilder module: DbContext and UnitOfWork refactoring
src/Modules/WorkflowBuilder/.../WorkflowBuilderDbContext.cs, src/Modules/WorkflowBuilder/.../WorkflowBuilderUnitOfWork.cs
WorkflowBuilderDbContext now inherits DbContext and registers the tenant schema interceptor in OnConfiguring (removing prior base model call). WorkflowBuilderUnitOfWork inlines domain-event orchestration, persistence, exception mapping, and publishing.
WorkflowEngine module: DbContext and UnitOfWork refactoring
src/Modules/WorkflowEngine/.../WorkflowEngineDbContext.cs, src/Modules/WorkflowEngine/.../WorkflowEngineUnitOfWork.cs
WorkflowEngineDbContext switches to DbContext with tenant interceptor registration; WorkflowEngineUnitOfWork is rewritten to aggregate domain events, persist with exception translation, and publish events via Wolverine.
Epic documentation confirmation updates
docs/epics/E03-data-modeling/README.md, docs/epics/E04-workflow-builder/README.md, docs/epics/E05-form-builder/README.md, docs/epics/E06-workflow-engine/README.md
Epic README implementation-status rows updated to note that DbContext and UnitOfWork were inlined per ADR-017 and to adjust infrastructure notes.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • phuong-labs/axis#37: Handlers updated to catch UniqueConstraintException and return conflict results, tied to this PR’s inlined UnitOfWork exception mapping.
  • phuong-labs/axis#25: Prior changes to shared UnitOfWork behavior for Postgres 23505 unique-constraint handling; this PR moves that behavior into per-module UnitOfWork implementations.

Poem

🐰 I hopped through docs and code today,
Each module took its work away—
Interceptors inline, events set free,
Units of work now live locally. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: inlining UnitOfWork and AxisDbContext per module following ADR-017, which is the core refactoring effort.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/axis-shared-abstractions-only

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (4)
docs/epics/E04-workflow-builder/README.md (1)

79-79: ⚡ Quick win

Use layer-level summary in Implementation Status notes.

This Infrastructure note includes per-component detail beyond the requested layer-summary format. Please condense to high-level status + ADR-017 confirmation.

As per coding guidelines, "Update epic README table and PROGRESS.md (layer summary only — not per-class detail) when implementation status changes per layer."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/epics/E04-workflow-builder/README.md` at line 79, Replace the
Infrastructure cell's per-component detail with a concise layer-level summary:
state that the Infrastructure layer is complete (or ✅ Done), confirm schema is
managed via EF Core migrations and that DbContext/UnitOfWork follow ADR-017, and
remove class-level specifics such as WorkflowBuilderDbContext,
WorkflowRepository, JSONB details, the 7 integration tests, and
InternalsVisibleTo; keep a brief note that integration tests and migrations
exist if you must mention them but only as high-level confirmation (e.g.,
"integration tests present" and "migrations applied").
docs/epics/E05-form-builder/README.md (1)

83-83: ⚡ Quick win

Keep Infrastructure status at layer granularity.

Please simplify this note to layer-level implementation status instead of detailed component-level inventory.

As per coding guidelines, "Update epic README table and PROGRESS.md (layer summary only — not per-class detail) when implementation status changes per layer."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/epics/E05-form-builder/README.md` at line 83, The Infrastructure row
currently lists component-level details (FormBuilderDbContext, FormSubmission EF
config + repository, FormStepReachedHandler scheduling,
AddFormWorkflowReferences migration, inlined DbContext + UnitOfWork per
ADR-017); replace that detailed inventory with a single layer-level status
phrase (e.g., "Infrastructure | ✅ Done" or "Infrastructure | In progress") and
remove per-class/component mentions so the README and PROGRESS.md reflect only
the layer summary as required by the coding guidelines.
docs/epics/E03-data-modeling/README.md (1)

86-86: ⚡ Quick win

Trim Infrastructure notes to layer-level status summary.

This row is overly granular (class/member-level detail) for a layer status update. Please keep it to concise layer-level progress and ADR linkage.

As per coding guidelines, "Update epic README table and PROGRESS.md (layer summary only — not per-class detail) when implementation status changes per layer."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/epics/E03-data-modeling/README.md` at line 86, The "Infrastructure" row
in the epic README is too granular—remove class/member-level details like
GetPagedAsync, BulkDeleteAsync, GetAllForExportAsync, DbContext + UnitOfWork
inlining, and the InternalsVisibleTo("Axis.Api") note, and replace them with a
concise layer-level status summary (e.g., "Infrastructure: ✅ Done — EF Core
mappings, repositories, JSONB converters implemented; see ADR-017 for
DbContext/UoW approach and E01 US-003 for tenant migration notes") that
references ADR-017 and E01 US-003 but avoids per-class implementation specifics.
docs/PROGRESS.md (1)

30-30: ⚡ Quick win

Keep PROGRESS updates at layer summary level only.

This line now includes class-by-class detail, which makes PROGRESS.md drift from the “layer summary only” contract. Please condense to a layer-level statement (e.g., “Shared.Infrastructure narrowed to cross-cutting concerns per ADR-017”).

As per coding guidelines, "Update epic README table and PROGRESS.md (layer summary only — not per-class detail) when implementation status changes per layer."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/PROGRESS.md` at line 30, The PROGRESS.md line is too granular — it lists
individual classes (Axis.Shared.Domain, Axis.Shared.Application,
Axis.Shared.Infrastructure, TenantSchemaInterceptor, HttpTenantContext,
HandlerLoggingMiddleware, UnitOfWork, AxisDbContext) instead of a layer-level
summary; replace that sentence with a single-layer statement such as
“Shared.Infrastructure narrowed to cross-cutting concerns per ADR-017” (or
equivalent) and remove the per-class detail so the file stays at the prescribed
layer-summary level referenced by ADR-017 and the epic README update guideline.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/epics/E06-workflow-engine/README.md`:
- Line 89: Replace the informal "real dispatch deferred" note in the epic
summary with an explicit deferred-followup callout using the required format:
change the phrase in the line containing "`IScriptExecutor` and
`INotificationSender` are stubs (real dispatch deferred).`" to a callout like
"`**Deferred (PR `#N` follow-up):** Implement real dispatch for IScriptExecutor
and INotificationSender; current stubs remain.`" ensuring you include the PR
placeholder `#N` and mention the specific components `IScriptExecutor` and
`INotificationSender`.

In
`@src/Modules/DataModeling/Axis.DataModeling.Infrastructure/Persistence/DataModelingUnitOfWork.cs`:
- Around line 43-44: The event publish loop currently calls await
bus.PublishAsync(evt) without the CancellationToken; change the loop in
DataModelingUnitOfWork so each PublishAsync forwards the ct passed to
SaveChangesAsync (e.g., await bus.PublishAsync(evt, ct)) and ensure the method
signature exposes/receives that CancellationToken and uses it for both
SaveChangesAsync and bus.PublishAsync when publishing IDomainEvent instances.

---

Nitpick comments:
In `@docs/epics/E03-data-modeling/README.md`:
- Line 86: The "Infrastructure" row in the epic README is too granular—remove
class/member-level details like GetPagedAsync, BulkDeleteAsync,
GetAllForExportAsync, DbContext + UnitOfWork inlining, and the
InternalsVisibleTo("Axis.Api") note, and replace them with a concise layer-level
status summary (e.g., "Infrastructure: ✅ Done — EF Core mappings, repositories,
JSONB converters implemented; see ADR-017 for DbContext/UoW approach and E01
US-003 for tenant migration notes") that references ADR-017 and E01 US-003 but
avoids per-class implementation specifics.

In `@docs/epics/E04-workflow-builder/README.md`:
- Line 79: Replace the Infrastructure cell's per-component detail with a concise
layer-level summary: state that the Infrastructure layer is complete (or ✅
Done), confirm schema is managed via EF Core migrations and that
DbContext/UnitOfWork follow ADR-017, and remove class-level specifics such as
WorkflowBuilderDbContext, WorkflowRepository, JSONB details, the 7 integration
tests, and InternalsVisibleTo; keep a brief note that integration tests and
migrations exist if you must mention them but only as high-level confirmation
(e.g., "integration tests present" and "migrations applied").

In `@docs/epics/E05-form-builder/README.md`:
- Line 83: The Infrastructure row currently lists component-level details
(FormBuilderDbContext, FormSubmission EF config + repository,
FormStepReachedHandler scheduling, AddFormWorkflowReferences migration, inlined
DbContext + UnitOfWork per ADR-017); replace that detailed inventory with a
single layer-level status phrase (e.g., "Infrastructure | ✅ Done" or
"Infrastructure | In progress") and remove per-class/component mentions so the
README and PROGRESS.md reflect only the layer summary as required by the coding
guidelines.

In `@docs/PROGRESS.md`:
- Line 30: The PROGRESS.md line is too granular — it lists individual classes
(Axis.Shared.Domain, Axis.Shared.Application, Axis.Shared.Infrastructure,
TenantSchemaInterceptor, HttpTenantContext, HandlerLoggingMiddleware,
UnitOfWork, AxisDbContext) instead of a layer-level summary; replace that
sentence with a single-layer statement such as “Shared.Infrastructure narrowed
to cross-cutting concerns per ADR-017” (or equivalent) and remove the per-class
detail so the file stays at the prescribed layer-summary level referenced by
ADR-017 and the epic README update guideline.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 157711e8-408f-4dc7-9961-dc387a73f45c

📥 Commits

Reviewing files that changed from the base of the PR and between 201bf5d and baec85b.

📒 Files selected for processing (17)
  • docs/PROGRESS.md
  • docs/epics/E03-data-modeling/README.md
  • docs/epics/E04-workflow-builder/README.md
  • docs/epics/E05-form-builder/README.md
  • docs/epics/E06-workflow-engine/README.md
  • src/Modules/DataModeling/Axis.DataModeling.Infrastructure/Persistence/DataModelingDbContext.cs
  • src/Modules/DataModeling/Axis.DataModeling.Infrastructure/Persistence/DataModelingUnitOfWork.cs
  • src/Modules/FormBuilder/Axis.FormBuilder.Infrastructure/Persistence/FormBuilderDbContext.cs
  • src/Modules/FormBuilder/Axis.FormBuilder.Infrastructure/Persistence/FormBuilderUnitOfWork.cs
  • src/Modules/Identity/Axis.Identity.Infrastructure/Persistence/IdentityUnitOfWork.cs
  • src/Modules/WorkflowBuilder/Axis.WorkflowBuilder.Infrastructure/Persistence/WorkflowBuilderDbContext.cs
  • src/Modules/WorkflowBuilder/Axis.WorkflowBuilder.Infrastructure/Persistence/WorkflowBuilderUnitOfWork.cs
  • src/Modules/WorkflowEngine/Axis.WorkflowEngine.Infrastructure/Persistence/WorkflowEngineDbContext.cs
  • src/Modules/WorkflowEngine/Axis.WorkflowEngine.Infrastructure/Persistence/WorkflowEngineUnitOfWork.cs
  • src/Shared/Axis.Shared.Infrastructure/Persistence/AxisDbContext.cs
  • src/Shared/Axis.Shared.Infrastructure/Persistence/UnitOfWork.cs
  • tests/Shared/Axis.Shared.Infrastructure.Tests/Persistence/UnitOfWorkTests.cs
💤 Files with no reviewable changes (3)
  • src/Shared/Axis.Shared.Infrastructure/Persistence/UnitOfWork.cs
  • tests/Shared/Axis.Shared.Infrastructure.Tests/Persistence/UnitOfWorkTests.cs
  • src/Shared/Axis.Shared.Infrastructure/Persistence/AxisDbContext.cs

Comment thread docs/epics/E06-workflow-engine/README.md Outdated
Comment on lines +43 to +44
foreach (IDomainEvent evt in events)
await bus.PublishAsync(evt);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Forward CancellationToken to PublishAsync.

The ct parameter is forwarded to SaveChangesAsync but not to bus.PublishAsync. Wolverine's IMessageBus.PublishAsync accepts a CancellationToken.

Proposed fix
         foreach (IDomainEvent evt in events)
-            await bus.PublishAsync(evt);
+            await bus.PublishAsync(evt, ct);

As per coding guidelines: "Always forward CancellationToken through async method calls."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
foreach (IDomainEvent evt in events)
await bus.PublishAsync(evt);
foreach (IDomainEvent evt in events)
await bus.PublishAsync(evt, ct);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/Modules/DataModeling/Axis.DataModeling.Infrastructure/Persistence/DataModelingUnitOfWork.cs`
around lines 43 - 44, The event publish loop currently calls await
bus.PublishAsync(evt) without the CancellationToken; change the loop in
DataModelingUnitOfWork so each PublishAsync forwards the ct passed to
SaveChangesAsync (e.g., await bus.PublishAsync(evt, ct)) and ensure the method
signature exposes/receives that CancellationToken and uses it for both
SaveChangesAsync and bus.PublishAsync when publishing IDomainEvent instances.

…PublishAsync

CodeRabbit findings on PR #87:

1. **E06 README — required deferred callout format.** The existing
   "real dispatch deferred" phrase in the Infrastructure row should be
   the explicit `**Deferred (PR #N follow-up):**` callout per CLAUDE.md
   Definition of Done. Rewritten to the canonical format; mentions the
   intended implementations (Roslyn scripting + MailKit).

2. **Forward CancellationToken to PublishAsync — NOT VALID for our
   Wolverine version.** CodeRabbit suggested `bus.PublishAsync(evt, ct)`
   but the WolverineFx 5.39.3 signature is
   `PublishAsync(object, DeliveryOptions?)` — there is no
   CancellationToken overload. PublishAsync is fire-and-forget; it
   enqueues onto the outbox and returns. Added an inline comment in
   DataModelingUnitOfWork explaining why ct is intentionally absent so
   future reviewers (human or AI) don't re-flag.

Reply to CodeRabbit on the comment thread will note the verification:
adding ct to PublishAsync produces compile error CS1503 "cannot convert
from CancellationToken to DeliveryOptions?".
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant