diff --git a/skills/.gitkeep b/skills/.gitkeep deleted file mode 100644 index 8b13789..0000000 --- a/skills/.gitkeep +++ /dev/null @@ -1 +0,0 @@ - diff --git a/skills/dotnet/zerya-dotnet-ddd-architecture/SKILL.md b/skills/dotnet/zerya-dotnet-ddd-architecture/SKILL.md new file mode 100644 index 0000000..4bf1b38 --- /dev/null +++ b/skills/dotnet/zerya-dotnet-ddd-architecture/SKILL.md @@ -0,0 +1,50 @@ +--- +name: zerya-dotnet-ddd-architecture +description: Discover, design, refactor, or review .NET domain models with strategic and tactical DDD. Use when clarifying ubiquitous language, subdomains, Bounded Contexts, context relationships, aggregate and entity boundaries, invariants, domain services, application processes, domain events, or misplaced business rules. Use it to diagnose domain-model problems before choosing persistence or modular-monolith boundaries; use a persistence- or framework-specific skill only after the domain decision is explicit. +--- + +# Zerya .NET DDD Architecture + +Treat architecture as a set of evidence-backed model decisions, not a ladder from classes to projects. Keep semantic, consistency, ownership, and implementation boundaries distinct. + +## Establish domain evidence + +1. Read repository instructions and accepted architecture decisions. +2. Identify the business problem, actors, outcomes, policies, terminology, and disputed meanings. +3. Consult domain experts or existing domain artifacts when available. Mark boundaries inferred only from code as hypotheses. +4. Map the relevant subdomains, candidate Bounded Contexts, upstream/downstream relationships, and language translations. +5. Trace representative commands, queries, events, external imports, and failure cases through the current implementation. +6. Record true business invariants, acceptable inconsistency windows, ownership, and independent lifecycles. + +Read [strategic-modeling.md](references/strategic-modeling.md) when discovering or changing Bounded Contexts. Do not infer a context solely from folders, tables, teams, or deployment units. + +## Make separate decisions + +Decide each axis on its own evidence: + +- **Model meaning:** define where one ubiquitous language and internally consistent model apply. +- **Consistency:** cluster only state that must satisfy named invariants atomically. +- **Behavior:** place decisions with the domain concept that has the information and responsibility to make them. +- **Ownership:** assign authoritative writes and lifecycle responsibility explicitly. +- **Implementation:** choose namespaces, projects, DbContexts, schemas, or processes only to enforce already justified boundaries. + +For aggregate design, read [aggregate-boundaries.md](references/aggregate-boundaries.md). For behavior and rule placement, read [ddd-and-rule-placement.md](references/ddd-and-rule-placement.md). For coordination across aggregates, read [application-processes.md](references/application-processes.md). + +## Preserve domain integrity + +- Keep invariants and state transitions in the domain model. +- Reference other aggregates by identity unless a repository-specific mechanism preserves the same boundary without exposing mutable cross-aggregate graphs. +- Modify one aggregate instance per transaction as a strong default, not an absolute law. Name the invariant before making a multi-aggregate write atomic. +- Let Application load authoritative facts, authorize the use case, invoke domain decisions, and coordinate I/O. +- Use domain events for meaningful completed domain facts. Choose synchronous or asynchronous handling from consistency and delivery requirements, not from event naming alone. +- Keep simple CRUD simple when no valuable domain model is present. + +Read [authorization.md](references/authorization.md) only when permissions depend on domain state or resource ownership. + +## Route implementation work + +- Use `zerya-dotnet-persistence-evolution` after changing aggregate persistence, EF Core mappings, transactions, read models, schema, or data. +- Use `zerya-wolverine-vertical-slices` for Wolverine handlers, middleware, outbox, scheduling, or routing. +- Use `zerya-dotnet-modular-monolith` only after a business boundary and concrete isolation benefit are established. + +For reviews, read [architecture-review.md](references/architecture-review.md), report findings by severity with evidence and consequences, and distinguish facts from modeling hypotheses. Do not edit during a review unless requested. Read [sources.md](references/sources.md) only for provenance or further study. diff --git a/skills/dotnet/zerya-dotnet-ddd-architecture/agents/openai.yaml b/skills/dotnet/zerya-dotnet-ddd-architecture/agents/openai.yaml new file mode 100644 index 0000000..fd25587 --- /dev/null +++ b/skills/dotnet/zerya-dotnet-ddd-architecture/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Zerya .NET DDD Architecture" + short_description: "Discover and improve .NET domain models" + default_prompt: "Use $zerya-dotnet-ddd-architecture to discover or review this .NET domain model and its boundaries." diff --git a/skills/dotnet/zerya-dotnet-ddd-architecture/references/aggregate-boundaries.md b/skills/dotnet/zerya-dotnet-ddd-architecture/references/aggregate-boundaries.md new file mode 100644 index 0000000..a06b1f7 --- /dev/null +++ b/skills/dotnet/zerya-dotnet-ddd-architecture/references/aggregate-boundaries.md @@ -0,0 +1,35 @@ +# Aggregate Boundaries + +## Start with invariants + +Name the business rules that must remain true at the end of every successful transaction. Cluster only the state required to enforce those rules. Treat an aggregate as a consistency and change boundary, not an object graph, query shape, table group, or ownership label. + +For each candidate boundary establish: + +- the invariant protected by the root; +- the commands that may change it; +- the state required to decide those commands; +- the concurrency unit and expected contention; +- whether temporary inconsistency outside the boundary is acceptable; +- whether every external mutation can pass through the root. + +## Use secondary signals carefully + +Independent identity, lifecycle, import, correction, retention, query needs, large `Include` graphs, partial loading, and unrelated concurrency conflicts may expose a wrong boundary. None proves that an entity is a new aggregate root. + +Promote an entity to an aggregate root only when it must be independently addressed as a consistency boundary, owns meaningful invariants and lifecycle transitions, and can protect them without routing changes through the former root. + +Keep an entity inside an aggregate when the root must control its creation, mutation, or removal to preserve a named invariant. Keep a Value Object inside when identity is irrelevant and replacement preserves its semantics. + +## Design the interaction + +After splitting an aggregate: + +- reference the other aggregate by identity; +- enforce each aggregate's local invariants internally; +- load authoritative external facts in Application and pass domain concepts to a named domain policy when the business decision spans aggregates; +- define stale-data tolerance when a decision uses a snapshot or projection; +- use optimistic concurrency on the boundary whose invariant is changing; +- create a read model when a use case needs a combined query shape. + +Do not merge aggregates merely to make a screen, report, import, or orchestration step convenient. Do not split them merely because a collection is large; first determine whether the collection participates in an invariant and whether the model can represent that invariant without loading every member. diff --git a/skills/dotnet/zerya-dotnet-ddd-architecture/references/application-processes.md b/skills/dotnet/zerya-dotnet-ddd-architecture/references/application-processes.md new file mode 100644 index 0000000..abecd0b --- /dev/null +++ b/skills/dotnet/zerya-dotnet-ddd-architecture/references/application-processes.md @@ -0,0 +1,28 @@ +# Application Processes + +Use an application process when a use case coordinates aggregates, repositories, authorization, external ports, or durable steps. Keep coordination in Application and business decisions in the domain model. + +## Coordinate a local use case + +1. Resolve actor facts and reject coarse unauthorized access early. +2. Load the target resource and only the authoritative facts required for the decision. +3. Perform resource-based authorization without leaking existence or sensitive state. +4. Invoke aggregate behavior or a named domain policy. +5. Commit the chosen consistency boundary. +6. Persist outgoing facts durably when later reactions are required. + +Reading another aggregate does not add it to the write boundary. Pass only the facts or domain concepts required for the decision, and define whether they must be current. + +## Coordinate multiple writes + +Modify one aggregate instance per transaction as the default. When a use case appears to require more: + +1. Name the exact invariant and the state it protects. +2. Check whether the aggregates are wrongly split. +3. Check whether reservation, ordered steps, compensation, or temporary inconsistency satisfies the business. +4. Use a multi-aggregate local transaction only when a documented invariant truly requires atomicity and the operational platform supports it. +5. Use a durable process manager or saga when work spans time, retries, external systems, or compensation. + +Do not invent a synthetic aggregate solely to hide orchestration. Do not use domain events to disguise a reaction that must be atomic with the originating invariant. + +For long-running processes define state ownership, correlation, idempotency, timeout, retry, compensation, observability, and manual repair. diff --git a/skills/dotnet/zerya-dotnet-ddd-architecture/references/architecture-review.md b/skills/dotnet/zerya-dotnet-ddd-architecture/references/architecture-review.md new file mode 100644 index 0000000..9a21d12 --- /dev/null +++ b/skills/dotnet/zerya-dotnet-ddd-architecture/references/architecture-review.md @@ -0,0 +1,23 @@ +# DDD Architecture Review + +Review domain evidence before code structure. + +1. State the business capability and vocabulary in scope. +2. Trace representative commands, decisions, failures, events, and queries end to end. +3. Map Bounded Context hypotheses, context relationships, aggregate lifecycles, invariants, consistency needs, and ownership. +4. Compare declared rules with actual code, persistence, tests, and production constraints. +5. Distinguish model defects from persistence coupling, framework conventions, and missing technical enforcement. +6. Recommend the smallest model correction, then route implementation consequences to the relevant skill. + +Classify findings as ambiguous language, context leakage, incorrect aggregate boundary, false invariant, misplaced domain rule, procedural domain logic, ownership ambiguity, consistency mismatch, or accidental implementation coupling. + +For every material finding provide: + +- concrete file and line evidence; +- the affected business behavior; +- the violated or unsupported model assumption; +- consequence and realistic failure mode; +- smallest viable correction; +- verification through examples, tests, or expert confirmation. + +Label conclusions inferred only from code as hypotheses. Do not equate class size, folder structure, table count, or project layout with a Bounded Context. diff --git a/skills/dotnet/zerya-dotnet-ddd-architecture/references/authorization.md b/skills/dotnet/zerya-dotnet-ddd-architecture/references/authorization.md new file mode 100644 index 0000000..263974d --- /dev/null +++ b/skills/dotnet/zerya-dotnet-ddd-architecture/references/authorization.md @@ -0,0 +1,14 @@ +# Authorization + +Keep authentication and authorization distinct. Authentication establishes actor facts; the use case decides whether that actor may act on a resource. + +- Resolve stable actor identity and coarse permissions at the boundary. +- Load the target resource in Application before resource-based authorization. +- Place reusable business permission rules in explicit policies with clear ownership. +- Keep HTTP status mapping and claims parsing outside Domain. +- Do not pass a mutable current-user service into aggregates. +- Avoid stale authorization projections for decisions requiring current ownership, assignment, or status. +- Test denial paths, information disclosure, tenant boundaries, and state-dependent permissions. + +If the rule is also a domain invariant, enforce the invariant in Domain even after an application-level authorization check. + diff --git a/skills/dotnet/zerya-dotnet-ddd-architecture/references/ddd-and-rule-placement.md b/skills/dotnet/zerya-dotnet-ddd-architecture/references/ddd-and-rule-placement.md new file mode 100644 index 0000000..3edd76a --- /dev/null +++ b/skills/dotnet/zerya-dotnet-ddd-architecture/references/ddd-and-rule-placement.md @@ -0,0 +1,48 @@ +# DDD and Business-Rule Placement + +Place a rule where its business meaning can be expressed and protected without infrastructure knowledge. Distinguish a business decision from loading data, sequencing calls, authorization, persistence, and transport mapping. + +## Entity or Aggregate + +Use behavior on an Entity or Aggregate Root when it protects that object's state, invariants, and transitions. Expose intention-revealing operations rather than public mutation. Do not force behavior onto an aggregate merely because the use case starts there. + +## Value Object + +Use a Value Object when identity does not matter and validity can be established at creation. Make equality structural and prevent partially valid states. Use types that reflect the ubiquitous language rather than wrapping every primitive mechanically. + +## Domain Policy or Domain Service + +Use a named domain policy when a business decision spans concepts but does not naturally belong to one entity. Pass it domain values or authoritative facts loaded by Application. Keep EF queries, network calls, logging, authorization infrastructure, and message publication outside the policy. + +Use a domain service sparingly. Give it a name from the ubiquitous language and keep it stateless unless state itself is a domain concept. + +## Application Service or Process + +Use Application to: + +- load aggregates and authoritative facts; +- perform resource authorization; +- invoke domain behavior and policies; +- coordinate repositories and external ports; +- define transaction and retry boundaries; +- translate completed domain outcomes into external messages. + +Application may enforce input, existence, sequencing, and operational rules. It must not become the only place where business meaning exists. + +## Handler or Endpoint + +Keep a handler thin enough that its flow reveals the use case. It may validate message shape, authorize, load state, invoke Application or domain behavior, map results, and arrange publication. Extract behavior when conditions, loops, status switches, fallback choices, or error interpretation encode domain meaning. + +## Events + +Raise a domain event for a meaningful completed fact in the domain model. Decide its handling mode separately: + +- handle synchronously when another reaction must complete inside the same consistency boundary and transaction; +- handle after commit when temporary inconsistency is acceptable; +- publish an integration event when the fact becomes a stable contract across a system or ownership boundary. + +Do not classify an event solely by namespace, process boundary, or transport. Document delivery, idempotency, ordering, and compatibility requirements for every asynchronous contract. + +## Avoid ceremony + +Use a rich domain model selectively. Straightforward CRUD and reference data may remain simple. Do not add empty repositories, forwarding services, factories without creation policy, or events for property changes that have no domain meaning. diff --git a/skills/dotnet/zerya-dotnet-ddd-architecture/references/sources.md b/skills/dotnet/zerya-dotnet-ddd-architecture/references/sources.md new file mode 100644 index 0000000..146f6b7 --- /dev/null +++ b/skills/dotnet/zerya-dotnet-ddd-architecture/references/sources.md @@ -0,0 +1,17 @@ +# Sources + +Open only the sources relevant to the decision. + +## Strategic DDD + +- [Eric Evans, DDD Reference](https://domainlanguage.com/wp-content/uploads/2016/05/DDD_Reference_2015-03.pdf) — definitions and pattern summaries for ubiquitous language, Bounded Context, context mapping, entities, Value Objects, Services, and Aggregates. +- [Martin Fowler, Bounded Context](https://martinfowler.com/bliki/BoundedContext.html) — concise explanation of model consistency and language boundaries. +- [Martin Fowler, Domain-Driven Design](https://martinfowler.com/bliki/DomainDrivenDesign.html) — overview of domain modelling and strategic design. + +## Aggregates and tactical modelling + +- [Martin Fowler, DDD Aggregate](https://martinfowler.com/bliki/DDD_Aggregate.html) — aggregate root, integrity, persistence, and transaction boundary. +- [Vaughn Vernon, Implementing Domain-Driven Design sample](https://www.informit.com/content/images/9780321834577/samplepages/0321834577.pdf) — true invariants, small aggregates, identity references, and eventual consistency. +- [Microsoft, Design a microservice domain model](https://learn.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/microservice-domain-model) — .NET-oriented aggregate and domain-model guidance; apply the tactical concepts without assuming a microservice deployment. + +Treat all patterns as inputs to collaborative modelling. Prefer observed business language, examples, invariants, and consistency requirements over pattern matching. diff --git a/skills/dotnet/zerya-dotnet-ddd-architecture/references/strategic-modeling.md b/skills/dotnet/zerya-dotnet-ddd-architecture/references/strategic-modeling.md new file mode 100644 index 0000000..ae516a2 --- /dev/null +++ b/skills/dotnet/zerya-dotnet-ddd-architecture/references/strategic-modeling.md @@ -0,0 +1,36 @@ +# Strategic Domain Modeling + +## Discover the model + +Work with domain experts, product language, process descriptions, examples, policies, events, and observed code. Capture: + +- business outcomes and decisions; +- terms whose meaning changes by workflow, department, customer, or lifecycle stage; +- core, supporting, and generic subdomains; +- authoritative sources and ownership; +- upstream/downstream relationships and required translations; +- places where one model cannot remain internally consistent. + +Treat event storming, example mapping, process modelling, and code archaeology as complementary discovery techniques. Do not let any workshop notation become the architecture automatically. + +## Propose Bounded Contexts + +Define a Bounded Context around one internally consistent model and ubiquitous language. For each candidate record: + +```text +Purpose and business outcome: +Ubiquitous language and ambiguous terms: +Model and policy owner: +Authoritative data and writes: +Upstream and downstream contexts: +Translation or anti-corruption needs: +Evidence, assumptions, and unresolved questions: +``` + +Use organizational, lifecycle, regulatory, scaling, and change-cadence evidence to challenge a boundary, not to replace semantic modelling. A team, namespace, schema, service, or module may implement a Bounded Context but does not define one by itself. + +## Map relationships + +Name the relationship and direction explicitly. Decide whether the downstream conforms, translates through an anti-corruption layer, shares a published language, or requires another context-map pattern. Avoid a shared kernel unless the teams deliberately accept joint model ownership and coordinated change. + +Validate proposed contexts against real scenarios and language. Keep uncertain boundaries reversible and revisit them as the model evolves. diff --git a/skills/dotnet/zerya-dotnet-modular-monolith/SKILL.md b/skills/dotnet/zerya-dotnet-modular-monolith/SKILL.md new file mode 100644 index 0000000..7a44284 --- /dev/null +++ b/skills/dotnet/zerya-dotnet-modular-monolith/SKILL.md @@ -0,0 +1,57 @@ +--- +name: zerya-dotnet-modular-monolith +description: Design, implement, refactor, or review hard or soft module boundaries in an accepted .NET modular monolith. Use when a repository already uses or has explicitly chosen a modular monolith, when extracting a justified business capability, enforcing compiler boundaries, introducing module-owned DbContexts or schemas, defining Contracts, Queries and Integration Events, composing cross-module reads, orchestrating per-module migrations, or testing module dependencies. Do not use as the first response to an uncertain domain boundary, isolated EF migration problem, or Wolverine handler problem; diagnose those with the focused DDD, persistence, or Wolverine skill first. +--- + +# Zerya .NET Modular Monolith + +Implement already-justified module boundaries. Before introducing or splitting a module, use `zerya-dotnet-ddd-architecture` to confirm the semantic boundary, `zerya-dotnet-persistence-evolution` for storage-only problems, and `zerya-wolverine-vertical-slices` for framework-only problems. Confirm that the proposed module is not merely an aggregate, entity, feature folder, integration adapter, persistence concern, or handler convention. + +## Confirm the boundary + +Proceed only when the repository already accepts modular monolith architecture or evidence shows a concrete need for module isolation. Record: + +```text +Capability and business owner: +Reason a feature or separate aggregate is insufficient: +Write and transaction owner: +Data, DbContext, schema, and migration owner: +Public Queries and Integration Events: +Cross-module read strategy: +Authorization owner: +Project references and enforcement: +Migration and rollout sequence: +``` + +Choose the least rigid effective level: + +- Use feature boundaries for namespaces and ownership within existing projects. +- Use soft modules for separate DbContexts, optional schemas, internal visibility, and architecture tests without projects per module. +- Use hard modules for compiler-enforced projects, Contracts, isolated persistence, and explicit communication. + +## Enforce module ownership + +- Give each business rule, write model, table, mapping, migration, and repair process one module owner. +- Keep Domain free of EF Core, ASP.NET, Wolverine, Infrastructure, and foreign modules. +- Let modules depend on another module only through producer-owned stable Contracts. +- Keep Contracts limited to immutable Integration Events, public Queries, and transport-neutral responses. Do not expose Commands, handlers, domain entities, DbContexts, or services. +- Never mutate another module's write model or use its DbContext. Avoid cross-module EF navigations and distributed local transactions. +- Publish completed facts as Integration Events when asynchronous reaction is appropriate. Use producer-owned Queries only for justified current semantics or authorization. +- Keep synchronous dependencies explicit, acyclic, and observable. +- Prefer compiler boundaries and access modifiers, then architecture tests, then review rules. + +Read [architecture-model.md](references/architecture-model.md), [module-boundary-selection.md](references/module-boundary-selection.md), and [module-layout.md](references/module-layout.md) when defining structure or references. + +## Route only to needed guidance + +- For Host-facing APIs and Contracts, read [module-api.md](references/module-api.md). +- For Queries, Integration Events, outbox/inbox, retries, idempotency, and ordering, read [module-integration.md](references/module-integration.md). +- For module-owned DbContexts, schemas, migration histories, and write isolation, read [modular-persistence.md](references/modular-persistence.md). +- For cross-schema joins, SQL views, producer Queries, composition layers, and projections, read [cross-schema-read-models.md](references/cross-schema-read-models.md). +- For identity facts and resource authorization across boundaries, read [modular-authorization.md](references/modular-authorization.md). +- For deployment-time migration ordering and compatibility, read [migration-orchestration.md](references/migration-orchestration.md). +- For project rules and architecture tests, read [architecture-enforcement.md](references/architecture-enforcement.md). +- For review tasks, read [modular-review-checklist.md](references/modular-review-checklist.md). +- Read [sources.md](references/sources.md) only for provenance. + +Do not load every reference by default. Do not introduce Integration Events, a schema, or a project per module without connecting it to the selected boundary and consistency model. diff --git a/skills/dotnet/zerya-dotnet-modular-monolith/agents/openai.yaml b/skills/dotnet/zerya-dotnet-modular-monolith/agents/openai.yaml new file mode 100644 index 0000000..530701d --- /dev/null +++ b/skills/dotnet/zerya-dotnet-modular-monolith/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Zerya .NET Modular Monolith" + short_description: "Implement justified .NET module boundaries" + default_prompt: "Use $zerya-dotnet-modular-monolith to implement or review these justified module boundaries." diff --git a/skills/dotnet/zerya-dotnet-modular-monolith/references/architecture-enforcement.md b/skills/dotnet/zerya-dotnet-modular-monolith/references/architecture-enforcement.md new file mode 100644 index 0000000..b1e0ed1 --- /dev/null +++ b/skills/dotnet/zerya-dotnet-modular-monolith/references/architecture-enforcement.md @@ -0,0 +1,163 @@ +# Architecture Enforcement + +## Contents + +- [Enforcement order](#enforcement-order) +- [Project, namespace, and public API tests](#project-reference-tests) +- [Integration and data ownership](#integration-boundary-tests) +- [Read models and Wolverine](#readmodels-tests) +- [Test matrix and ADR triggers](#test-matrix) + +## Enforcement Order + +Prefer: + +1. compiler and project references; +2. access modifiers; +3. architecture tests; +4. static analysis; +5. code review and ADRs. + +Do not rely on documentation alone. + +## Project Reference Tests + +Build a graph from `.csproj` references and assert: + +- Domain references only SharedKernel and permitted framework-neutral packages; +- Contracts references only SharedKernel and framework-neutral contract packages; +- Application references own Domain, own Contracts, SharedKernel, and foreign Contracts only for Queries invoked or Events consumed; +- Infrastructure references only own Application, own Domain, and SharedKernel; +- ReadModels references SharedKernel and foreign Contracts only for producer Queries or event-fed projections it consumes; +- Host references Application, Infrastructure, ReadModels, and SharedKernel; +- no Application-to-Application references; +- no module references foreign Domain or Infrastructure; +- the directed graph of cross-module Query dependencies is acyclic. + +## Namespace and Assembly Tests + +Assert: + +- `MOTOQ.Module.X.*` namespaces live in module X projects; +- Domain does not reference EF Core, ASP.NET, Wolverine, HTTP, Contracts, or Infrastructure; +- Contracts contains only Query requests, Query responses, Integration Events, and contract-specific primitives, with no handlers or framework dependencies; +- Infrastructure types do not leak into Application responses; +- SharedKernel contains no module vocabulary. + +## Public API Tests + +Allow public Application request and response types intended for Host. + +Flag foreign references to: + +```text +*Handler +*Validator +*Policy +*Service +*DbContext +*Configuration +``` + +Handler classes may be public for Wolverine, but Host and modules must never call them directly. + +## Integration Boundary Tests + +Assert: + +- modules reference only foreign `*.Contracts`; +- no Contracts project contains Commands, handlers, validators, services, repositories, domain types, EF types, or Wolverine APIs; +- foreign `InvokeAsync` targets only `Contracts.Queries`; +- no module invokes a foreign Command, including through an allowlist or ADR exception; +- no Query is published and no Integration Event is invoked; +- foreign Query dependencies are acyclic and do not form nested fan-out chains; +- no domain event type is referenced outside its module; +- Integration Event handlers live in the consumer's Application or in ReadModels when implementing a justified event-fed projection. + +Avoid brittle source-text tests when project-reference or assembly analysis can enforce the rule more accurately. + +## Data Ownership Tests + +Check for: + +- foreign module DbContext types; +- foreign schema names in business-module runtime SQL; +- EF navigation to foreign domain types; +- migrations altering another module's tables; +- a global write DbContext; +- writes from ReadModels to module-owned tables; +- SharedKernel persistence abstractions. + +Allow foreign schema names only in dedicated ReadModels, coordinated migrations or administrative scripts with explicit ownership. Treat direct table and column use as an accepted persistence dependency. + +## ReadModels Tests + +Assert: + +- ReadModels references no module Application, Domain, or Infrastructure; +- cross-schema read rows are minimal persistence representations, not imported domain entities; +- one LINQ query does not combine `IQueryable` instances from different DbContexts; +- `ApplicationReadDbContext` does not own migrations for module source tables and cannot persist changes to them; +- persisted projections use a separate projection-owned write context that maps and migrates only projection-owned tables; +- cross-schema queries, views, migrations, and administrative scripts do not mutate module-owned source tables; +- producer-owned views are migrated by their producer and expose only intentional fields; +- sensitive reads use producer-owned authorization or demonstrate an equivalent current-state policy; +- event handlers are idempotent when a justified event-fed projection is used; +- projection backfills write only projection-owned tables. + +## Wolverine Verification + +When discovery or middleware changes, verify: + +- every intended message has one correct handler or the intended separated consumers; +- handler assemblies are included; +- transaction middleware targets the correct module context; +- every injected DbContext abstraction is explicitly mapped to its concrete context in Wolverine and resolves to the same scoped instance; +- Queries are non-transactional; +- validators are discovered; +- outgoing Integration Events use durable behavior where required; +- cross-module Queries use `InvokeAsync`, while Integration Events use `PublishAsync` or the repository-equivalent Wolverine APIs. + +Use repository-supported Wolverine diagnostics. + +## Test Matrix + +### Domain unit tests + +Use for invariants, value objects, transitions, policies, and domain-event production. + +### Application tests + +Use for use-case orchestration and authorization behavior. + +### EF SQL Server integration tests + +Use for projections, tracking, concurrency, JSON, migrations, and graph persistence. + +### Wolverine pipeline tests + +Use for validation, transactions, rollback, outbox, inbox, retry, routing, and event consumers. + +### Cross-module read-model tests + +Use SQL Server integration tests for joins, null handling, filtering, paging, generated SQL, schema mappings, and migration exclusions. For event-fed projections, also test completeness, idempotency, ordering assumptions, and eventual consistency. + +### Host/API tests + +Use for HTTP mapping, authentication plumbing, OpenAPI, and delegation. + +## ADR Triggers + +Create or update an ADR when: + +- adding or splitting a module; +- transferring data ownership; +- introducing a new cross-module Integration Event dependency; +- introducing a new synchronous cross-module Query dependency; +- adding a new persisted event-fed cross-module projection; +- moving a cross-module read to independent physical storage; +- allowing a temporary Host composition; +- introducing a repository exception; +- sharing a new business primitive; +- changing transaction or delivery semantics; +- adding special event ordering behavior. diff --git a/skills/dotnet/zerya-dotnet-modular-monolith/references/architecture-model.md b/skills/dotnet/zerya-dotnet-modular-monolith/references/architecture-model.md new file mode 100644 index 0000000..0b45d86 --- /dev/null +++ b/skills/dotnet/zerya-dotnet-modular-monolith/references/architecture-model.md @@ -0,0 +1,237 @@ +# Architecture Model + +## Contents + +- [Deployment and project set](#byoi-deployment) +- [Dependency graph](#dependency-graph) +- [Host and read-model responsibilities](#host-responsibilities) +- [Feature-first layout](#feature-first-layout) +- [SharedKernel](#sharedkernel) + +## BYOI Deployment + +The system has one deployment unit, one process boundary, and one physical SQL Server database: + +```text +MOTOQ.Host + ├── Identity module + ├── DocumentCases module + ├── Payments module + ├── other business modules + └── MOTOQ.ReadModels +``` + +A module is a business capability, not a technical layer or CRUD group. + +Each module owns a separate SQL schema and write model. Deploy the application and run all module migrations together. Sharing the physical database permits direct read composition but does not transfer write ownership. + +## Module Project Set + +```text +MOTOQ.Module..Application +MOTOQ.Module..Domain +MOTOQ.Module..Infrastructure +MOTOQ.Module..Contracts +``` + +### Application + +Owns: + +- public Commands and Queries invoked by Host; +- response DTOs; +- Wolverine handlers; +- validation; +- local authorization orchestration; +- application services and processes; +- module-local read queries; +- ports implemented by Infrastructure; +- Integration Event consumers; +- translation of domain facts into public Integration Events. + +### Domain + +Owns: + +- aggregates and entities; +- value objects; +- invariants and state transitions; +- domain policies and services; +- domain errors; +- domain events. + +### Infrastructure + +Owns: + +- module DbContext; +- EF mappings and migrations; +- external clients and adapters; +- Wolverine and DI registration; +- infrastructure workers; +- technical configuration. + +### Contracts + +Owns the stable public messages produced or answered by the module. + +It contains: + +- synchronous Query contracts and their transport-neutral responses; +- immutable Integration Event contracts under `Events`; +- stable contract-specific primitives where necessary. + +It does not contain: + +- Commands; +- handlers; +- validators; +- domain entities; +- DbContexts; +- Wolverine APIs; +- application ports; +- service interfaces. + +## Dependency Graph + +```text +SharedKernel + -> no module projects + +Module.X.Domain + -> SharedKernel + +Module.X.Contracts + -> SharedKernel when needed + +Module.X.Application + -> Module.X.Domain + -> Module.X.Contracts + -> Module.Y.Contracts only for Queries invoked or Events consumed + -> SharedKernel + +Module.X.Infrastructure + -> Module.X.Application + -> Module.X.Domain + -> SharedKernel + +ReadModels + -> SharedKernel + -> Module.*.Contracts for producer Queries or justified event-fed projections + +Host + -> Module.*.Application + -> Module.*.Infrastructure + -> ReadModels + -> SharedKernel +``` + +No Application-to-Application dependency is allowed. + +Cross-module synchronous dependencies must be Query-only and acyclic. Integration Events remain asynchronous facts. Cross-module Commands are forbidden without exception in this architecture. An ADR may change the repository's target architecture, but it does not create a local exception to this skill; surface that conflict before implementation. + +## Host Responsibilities + +Host owns: + +- process startup and shutdown; +- dependency injection composition; +- Wolverine global setup and assembly registration; +- ASP.NET routing and HTTP mapping; +- authentication middleware; +- coarse endpoint policy plumbing; +- OpenAPI; +- health checks and observability composition; +- migration orchestration; +- installation-level configuration; +- delegation to module Commands, Queries, and ReadModels. + +Host does not own: + +- aggregate mutations; +- business workflows; +- resource-specific business authorization; +- module persistence mappings; +- cross-module data repair logic; +- business fallback rules; +- long-lived cross-module projection state. + +## ReadModels Responsibilities + +`MOTOQ.ReadModels` or `Application.ReadModels` composes read-only data from multiple module schemas. It normally owns query handlers, response models, minimal EF read rows, and a read-only `ApplicationReadDbContext`; source modules retain ownership of the mapped tables. + +Default to a direct read-only cross-schema read model in this composition layer. Use producer-owned SQL views when producers must stabilize exposed database shapes and producer-owned Wolverine Queries when current producer semantics or authorization is required. Treat event-fed projections as a last resort for demonstrated quality-attribute or integration requirements that simpler cross-schema reads, views, or producer Queries cannot satisfy. Account for backfill and repair complexity before selecting one. Record the reason and trade-off when departing from the default. + +It is not: + +- a bounded context; +- a domain module; +- a write-side source of truth; +- an owner of mapped source tables; +- a way to mutate module data or bypass module invariants. + +When a justified event-fed projection persists state, keep its write model separate from `ApplicationReadDbContext`. Use a projection-owned `ProjectionWriteDbContext` that maps and migrates only projection-owned tables. Query code may map those tables read-only in `ApplicationReadDbContext`, but no projection write may weaken the read context's `SaveChanges` guard. + +## Feature-First Layout + +```text +MOTOQ.Module.Identity.Application/ + Roles/ + CreateRole.cs + DeleteRole.cs + GetUserRoles.cs + Users/ + Groups/ + +MOTOQ.Module.Identity.Domain/ + Roles/ + Role.cs + RoleId.cs + RoleErrors.cs + Users/ + Groups/ + +MOTOQ.Module.Identity.Infrastructure/ + Roles/ + RoleConfiguration.cs + Users/ + Groups/ + Persistence/ + IdentityDbContext.cs + Migrations/ + ModuleRegistration.cs + +MOTOQ.Module.Identity.Contracts/ + Queries/ + GetUserDisplayName.cs + Events/ + UserDisabledIntegrationEvent.cs + UserDisplayNameChangedIntegrationEvent.cs +``` + +Do not force empty parallel folders. Create feature folders where code exists. + +## SharedKernel + +Allowed candidates: + +- entity and aggregate-root primitives; +- value-object equality primitives; +- domain-event marker; +- strongly typed identifier primitive; +- small framework-neutral guard primitives; +- stable cross-cutting clock or actor primitives only when truly ubiquitous. + +Forbidden: + +- `User`, `Invoice`, `Payment`, `DocumentCase`, or other module concepts; +- Commands and Queries; +- concrete module Contracts; +- response DTOs; +- DbContext abstractions; +- external-system clients; +- generic repositories; +- module facades; +- cross-module authorization policies. + +Prefer small duplication over premature global coupling. diff --git a/skills/dotnet/zerya-dotnet-modular-monolith/references/cross-schema-read-models.md b/skills/dotnet/zerya-dotnet-modular-monolith/references/cross-schema-read-models.md new file mode 100644 index 0000000..22959c2 --- /dev/null +++ b/skills/dotnet/zerya-dotnet-modular-monolith/references/cross-schema-read-models.md @@ -0,0 +1,17 @@ +# Cross-Schema Read Models + +Keep cross-module composition read-only and outside business write models. + +Choose in this order unless requirements indicate otherwise: + +1. Direct read-only cross-schema projection in a dedicated composition layer. +2. Producer-owned SQL view when the producer must stabilize exposed storage shape. +3. Producer-owned Query when current semantics or authorization belongs to the producer. +4. Event-fed projection for demonstrated performance, availability, denormalization, search, analytics, scaling, or independent-storage needs. + +Record every directly read schema, table, and column. Never use composition to mutate foreign data, load foreign domain entities for updates, or bypass invariants. + +Do not combine LINQ queryables from different DbContext instances. Map all tables used by one direct SQL query in one read-only context. Override or guard `SaveChanges` and give its database principal read-only permissions where practical. + +For event-fed projections, define freshness, idempotency, ordering, replay, backfill, repair, monitoring, and ownership before implementation. + diff --git a/skills/dotnet/zerya-dotnet-modular-monolith/references/migration-orchestration.md b/skills/dotnet/zerya-dotnet-modular-monolith/references/migration-orchestration.md new file mode 100644 index 0000000..aa247be --- /dev/null +++ b/skills/dotnet/zerya-dotnet-modular-monolith/references/migration-orchestration.md @@ -0,0 +1,15 @@ +# Migration Orchestration + +Give each module its own DbContext, migration set, and migration-history identity when persistence is modularized. Let the application deployment orchestrate them in a deterministic order. + +- Keep migrations owned by the module that owns the schema objects. +- Avoid one module altering another module's tables. +- Use compatible expand-contract changes across deployment boundaries. +- Apply prerequisite producer changes before consumer changes. +- Make data backfills restartable, observable, and independently verifiable. +- Define startup failure behavior; do not silently continue after a partial required migration. +- Test migration from supported production baselines, not only database creation from empty state. +- Coordinate cross-schema views after all referenced shapes exist and before dependent application code becomes active. + +Sharing one physical database does not create shared write ownership or justify a transaction spanning module DbContexts. + diff --git a/skills/dotnet/zerya-dotnet-modular-monolith/references/modular-authorization.md b/skills/dotnet/zerya-dotnet-modular-monolith/references/modular-authorization.md new file mode 100644 index 0000000..b5ebeb0 --- /dev/null +++ b/skills/dotnet/zerya-dotnet-modular-monolith/references/modular-authorization.md @@ -0,0 +1,111 @@ +# Modular Authorization + +## Contents + +- [Ownership and current user](#ownership-split) +- [Resource authorization](#resource-authorization) +- [Identity facts](#identity-facts) +- [Permission versus policy](#permission-versus-policy) +- [Eventually consistent data](#eventually-consistent-authorization-data) + +## Ownership Split + +Identity owns: + +- authentication; +- credentials and external identity-provider integration; +- users and subject identifiers; +- role, permission, group, or persona membership; +- current-user resolution; +- coarse permission facts. + +The resource-owning module owns: + +- whether a user may view or mutate a specific resource; +- resource relationship checks; +- ownership, assignment, workflow, accounting-lock, and lifecycle rules; +- business authorization using its own domain language. + +Host owns: + +- authentication middleware; +- extracting request identity; +- endpoint-level policy plumbing; +- mapping forbidden and unauthorized outcomes to HTTP. + +## Current User + +Use a request-scoped abstraction: + +```csharp +public interface ICurrentUser +{ + Guid UserId { get; } + IReadOnlySet Permissions { get; } +} +``` + +Resolve it at the request or message boundary. Do not make handlers depend directly on `HttpContext`. + +Background messages must carry or resolve actor information required for authorization or auditing. + +## Resource Authorization + +Place resource-specific authorization in the owning module: + +```text +DocumentCases: + CanViewDocumentCase + CanAllocateCost + CanForceApprove + +Payments: + CanGenerateTransfer + CanMarkPaid +``` + +A Wolverine middleware may load the resource and create a typed authorization context where many handlers share the same policy. + +The middleware calls module-owned policy; it does not become a generic home for business rules. + +## Identity Facts + +Other modules receive identity facts through Integration Events when they need local projections. They may invoke a narrow Identity Query from Contracts when current identity data is required and synchronous coupling is accepted. + +They must not: + +- access Identity tables; +- depend on ASP.NET Identity entities; +- use Identity DbContext; +- invoke Identity Application Queries that are not intentionally exposed in Contracts, or create cyclic or broad Identity Query dependencies; +- place resource-specific authorization into Identity merely because it mentions users. + +## Permission Versus Policy + +A coarse permission: + +```text +document-cases.force-approve +``` + +answers whether the actor may attempt an operation. + +A resource policy: + +```text +CanForceApprove(currentUser, documentCase) +``` + +also considers resource state and relationships. + +Both may be required. + +## Eventually Consistent Authorization Data + +Cross-module read models may compose non-sensitive visibility or display data directly, invoke a producer-owned Query, read a producer-owned SQL view, or contain projected data when justified. + +Use a producer-owned Query for sensitive resource detail when the owning module must evaluate current relationships, lifecycle state, tenant isolation, or another resource policy. A direct table read must not duplicate or bypass such a policy merely to avoid synchronous integration. + +Do not use stale projected authorization as the sole source for sensitive operations where delayed revocation is unacceptable. + +Commands and sensitive detail access must be authorized by the owning module against its current state. diff --git a/skills/dotnet/zerya-dotnet-modular-monolith/references/modular-persistence.md b/skills/dotnet/zerya-dotnet-modular-monolith/references/modular-persistence.md new file mode 100644 index 0000000..c8d8164 --- /dev/null +++ b/skills/dotnet/zerya-dotnet-modular-monolith/references/modular-persistence.md @@ -0,0 +1,268 @@ +# Modular Persistence + +## Contents + +- [Database model and context abstractions](#byoi-database-model) +- [Write side and module-local queries](#write-side) +- [Cross-module read options](#cross-module-read-model-default) +- [Read and projection contexts](#applicationreaddbcontext) +- [Query and ownership rules](#query-implementation) +- [Event-fed projections](#event-fed-projection) +- [Migrations, backfills, and recovery](#migrations-backfills-and-recovery) + +## BYOI Database Model + +Assume: + +- one physical SQL Server database; +- one SQL schema and write model per business module; +- one process and deployment unit; +- all module migrations run together during application deployment. + +The shared physical database is an intentional read-composition boundary. It does not create shared write ownership. + +Each module owns: + +- its schema and tables; +- write-side EF mappings and DbContext; +- migrations and seed data for its schema; +- invariants, writes, repair, and reconciliation behavior. + +Forbidden in a business module: + +- injecting another module's write DbContext; +- querying another module's `DbSet` from module Application or Infrastructure; +- cross-module EF navigation or cascade delete; +- altering another module's tables in its migrations; +- using a global write DbContext; +- spanning one write transaction across module schemas. + +Reference foreign concepts in module state by stable identifiers received through an explicit contract or Integration Event. + +## Module Context Abstractions + +Define module-local abstractions in Application: + +```csharp +public interface IIdentityReadDbContext +{ + IQueryable Users { get; } + IQueryable Roles { get; } +} + +public interface IIdentityDbContext : IIdentityReadDbContext +{ + DbSet TrackedUsers { get; } + DbSet TrackedRoles { get; } +} +``` + +Infrastructure implements these abstractions with the module's concrete DbContext. When a transactional handler depends on an abstraction, register it with Wolverine's DbContext-abstraction mapping and forward the DI abstraction to the same scoped concrete DbContext instance. Do not use a registration that creates a second DbContext instance. Otherwise inject the concrete DbContext. Verify the generated handler chain applies transaction and outbox middleware to the intended context. Do not place DbContext abstractions in SharedKernel. + +## Write Side + +Default: + +```text +tracked load +domain mutation +Wolverine transaction middleware +SaveChanges +outbox flush when messages are produced +``` + +Use only the owning module's write DbContext and transaction boundary. Do not call `Update()` on a detached graph mixing existing and new entities. + +Repositories are not the default. Introduce a specialized persistence abstraction only when it protects a real aggregate persistence need that direct context access cannot express clearly. Do not introduce `IRepository`. + +## Module-Local Query + +Keep a query inside a module when it uses only module-owned data: + +```csharp +var roles = await db.Roles + .Where(role => role.IsActive) + .OrderBy(role => role.Name) + .Select(role => new RoleItem(role.Id, role.Name)) + .ToListAsync(ct); +``` + +Use no tracking, execute filtering and paging in SQL, select only required columns, and return a feature-local response. Test provider-sensitive behavior against SQL Server. Do not create a separate projection table for an ordinary local query. + +## Cross-Module Read Model Default + +When a read combines data owned by multiple modules, place it in a dedicated composition layer such as: + +```text +MOTOQ.ReadModels +``` + +or: + +```text +MOTOQ.Application.ReadModels +``` + +Do not place cross-module composition in Host as a normal design. A temporary `MOTOQ.Host/ReadModels` location requires an accepted migration ADR with an owner and removal plan; keep business rules out of it and move it to the dedicated composition layer. + +Default to a direct read-only cross-schema read model in the dedicated composition layer. + +Prefer the simplest read style that preserves the required semantics: + +1. use a direct read-only cross-schema query for ordinary composition; +2. use a producer-owned SQL view to stabilize the exposed database shape while preserving current committed reads and efficient joins; +3. use a producer-owned Wolverine Query from Contracts to preserve current producer semantics or producer-owned authorization; +4. use an event-fed persisted projection only as a last resort for justified denormalization, performance, independent storage, availability, scaling, analytics, search, or external integration that the simpler styles cannot satisfy. + +Base the choice on required freshness and consistency, source-schema stability, data sensitivity and authorization ownership, change frequency, expected module separation, and lifecycle cost. For a persisted projection, include delivery, monitoring, migration, repair, and backfill complexity in that cost. Record the selected read style and its dependencies in all cases. When departing from the default, also record the reason and trade-off. + +Use a direct cross-schema query for simple compositional reads when current committed data is required, source schemas are sufficiently stable, coordinated migrations are acceptable, and no producer-owned interpretation or sensitive authorization is bypassed. + +The composition layer may read private module tables for query purposes. It does not own those tables and must not live inside the Domain, Application, or Infrastructure project of one business module. + +Treat every directly read schema, table, and column as an internal persistence dependency that has been deliberately accepted, not as full module encapsulation. Record its data owner and coordinated-migration expectation with the read model. + +## Producer-Owned SQL View + +Use a producer-owned SQL view when current committed data and efficient database joins are required but the producer should stabilize the exposed shape. Place the view definition and migration in the producer module, expose only intended columns and semantics, and treat the view as a versioned read contract. + +The composition layer may join producer-owned views from multiple schemas. It must not own or silently redefine those views. + +## Producer-Owned Wolverine Query + +Invoke a Query from the producer's Contracts when current producer semantics or resource authorization is required, the source schema changes frequently, the data is sensitive, or future module separation is plausible. + +The producer owns the contract and handler. Keep the response narrow, prevent synchronous dependency cycles and fan-out, and account for the temporal coupling even though all handlers currently run in one process. Do not use a foreign Command as a read workaround. + +## ApplicationReadDbContext + +Prefer one dedicated read-only EF Core context that maps minimal representations of every table participating in a query: + +```csharp +internal sealed class ApplicationReadDbContext( + DbContextOptions options) + : DbContext(options) +{ + public DbSet Documents => Set(); + public DbSet Payments => Set(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(builder => + { + builder.HasNoKey(); + builder.ToView("Documents", "documents"); + }); + + modelBuilder.Entity(builder => + { + builder.HasNoKey(); + builder.ToView("Payments", "payments"); + }); + } +} +``` + +`ToView` may point to a physical table to make the mapping read-only from EF Core's perspective. Alternatively, map keyed read rows and exclude their tables from read-context migrations with `ExcludeFromMigrations()`. Migration exclusion alone does not prevent writes, so retain the `SaveChanges` guard. Keep this context read-only even when ReadModels also owns a persisted projection. + +Enforce these rules: + +- map minimal read rows, not module domain entities; +- expose no write-oriented repository or context abstraction; +- use `AsNoTracking()` by default for keyed mappings; +- reject every `SaveChanges` or `SaveChangesAsync` call, for example through an override or interceptor; +- do not generate migrations for module-owned source tables; +- do not combine `IQueryable` instances from different DbContext instances; +- map every table required by one LINQ query in the same read context. + +## Projection Write Context + +Only after an event-fed projection passes the justification gate, use a separate projection-owned write context: + +```text +ApplicationReadDbContext + -> read-only mappings for module source tables and projection tables used by queries + -> rejects every SaveChanges call + +ProjectionWriteDbContext + -> maps only projection-owned tables + -> owns only projection migrations + -> used only by projection consumers, backfills, and repair operations +``` + +Never map a module-owned source table as writable in `ProjectionWriteDbContext`. Do not remove or weaken the `ApplicationReadDbContext` guard to support projection writes. If several projections have unrelated ownership or lifecycle, use separate write contexts rather than a global ReadModels write model. + +## Query Implementation + +Prefer EF Core and LINQ when they generate clear, efficient SQL: + +```csharp +var result = await ( + from document in db.Documents + join payment in db.Payments + on document.Id equals payment.DocumentId into payments + from payment in payments.DefaultIfEmpty() + where document.Id == query.DocumentId + select new DocumentOverviewResponse( + document.Id, + document.Number, + payment == null ? null : payment.Status)) + .SingleOrDefaultAsync(ct); +``` + +Use plain SQL, `FromSql`, a SQL view, or a stored procedure only when: + +- LINQ generates inefficient SQL; +- the query requires CTEs, window functions, or SQL Server-specific functions; +- exact SQL control is required; +- the LINQ form would be unreasonably difficult to understand. + +SQL views may join tables across module schemas. Treat the view definition and every direct cross-schema query as a read dependency on the referenced schemas and columns; cover important queries with SQL Server integration tests. + +## Read Ownership Versus Write Ownership + +A cross-schema read model owns its query contract, response, and composition logic. Source modules continue to own the underlying data. + +Never use the read context to: + +- perform update, insert, or delete operations against module tables; +- bypass aggregate invariants or module authorization; +- load domain entities for later modification; +- attach read rows to a module write context; +- create foreign keys, cascades, or runtime write dependencies across schemas. + +Every write uses the owning module's write model and transaction boundary. Externally requested mutations enter through a Host-facing Command. Local Integration Event consumers, process managers, administrative operations, backfills, and projection handlers may write directly to state they own without exposing a public Command. + +## Event-Fed Projection + +Do not introduce an event-fed persisted projection merely because a query crosses module boundaries or to make the design appear more decoupled. Prefer a direct cross-schema read, producer-owned SQL view, or producer-owned Query whenever it meets the requirement with less lifecycle complexity. Choose a projection only when a concrete requirement justifies the extra storage, delivery handling, eventual consistency, monitoring, repair, migration, and backfill cost, for example: + +- direct joins are measurably too expensive; +- strong denormalization or precomputation is required; +- source data resides in different physical databases; +- reads need independent scaling or storage; +- the projection must remain available independently of its source; +- the workflow explicitly accepts eventual consistency; +- the projection serves an external consumer; +- analytics, reporting, or search needs a specialized store. + +Before accepting an event-fed projection, sketch how existing data will be populated and repaired. Do not assume complete event history or replay. If reconstructing the projection requires complex historical business logic, multi-stage reconciliation, or fragile event ordering, prefer a direct read or producer-owned Query unless an explicit quality attribute still outweighs that cost. + +For an accepted event-fed projection, define its owner, source events, `ProjectionWriteDbContext`, persistence, consistency expectation, idempotency, failure monitoring, and migration, backfill, repair, and retirement plans. Use Integration Events and durable Inbox/Outbox behavior where delivery reliability is required. + +## Migrations, Backfills, and Recovery + +Do not require cross-module read models to support event replay. + +Treat backfill design as part of projection selection, not as follow-up implementation detail. Prefer reading current source tables during a controlled backfill over reconstructing history when that preserves the required semantics. Reject the projection when its initial population or repair logic is disproportionate to the read benefit and a direct read, view, or producer Query remains practical. + +Use: + +- SQL Server backup and restore for physical disaster recovery; +- coordinated migrations for schema evolution; +- controlled backfills for new fields or persisted projections; +- cross-schema joins in migrations, repair scripts, administrative operations, and dedicated read-only read models. + +Include a migration plan when a persisted schema or SQL view changes. Include a backfill plan when new or changed persisted projection data must be populated. A non-persisted query, response, filtering, ordering, or composition change requires neither unless its source schema also changes. + +A backfill may read any required module schema but may write through the projection-owned write context only to the target projection's owned tables. It must not mutate source module data or become a runtime cross-module write mechanism. diff --git a/skills/dotnet/zerya-dotnet-modular-monolith/references/modular-review-checklist.md b/skills/dotnet/zerya-dotnet-modular-monolith/references/modular-review-checklist.md new file mode 100644 index 0000000..5f6c8fd --- /dev/null +++ b/skills/dotnet/zerya-dotnet-modular-monolith/references/modular-review-checklist.md @@ -0,0 +1,122 @@ +# Modular Review Checklist + +## Contents + +- [Ownership and project references](#ownership) +- [Application API, handlers, and domain](#application-api) +- [Events and synchronous queries](#integration-events) +- [Local and cross-module reads](#local-reads) +- [Authorization and ordering](#authorization) +- [Verification](#verification) + +## Ownership + +- What business capability owns this behavior? +- Is a rule in Host or SharedKernel because ownership was unclear? +- Does the write touch exactly one module-owned persistence boundary? +- Is a foreign concept translated into local language? + +## Project References + +- Does Host reference only Application, Infrastructure, ReadModels, and SharedKernel? +- Does a module reference only foreign Contracts, and only for Queries invoked or Events consumed? +- Is the cross-module Query graph acyclic? +- Has any `Application -> Application` dependency appeared? +- Does ReadModels reference a module's Application, Domain, or Infrastructure? +- Does cross-module composition live in Host without a temporary migration ADR, owner, and removal plan? +- Does SharedKernel contain a concrete module contract? + +## Application API + +- Is the new Command or Query intended for Host? +- Can the contract expose less data? +- Does any code call a handler directly? +- Is a Command or Query being used by another module? + +## Handler + +- Is the happy path visible? +- Does it load local state, invoke named behavior, and return? +- Does it contain domain-status switches, workflow loops, or fallback logic? +- Does it call `SaveChangesAsync()` unnecessarily? +- If it changes state, does it use only the owning module's write DbContext and transaction boundary? +- If it injects a DbContext abstraction, does Wolverine map it to the concrete context and does DI resolve both to the same scoped instance? +- Does it enlist another module's DbContext or write to another module's schema? +- Is Wolverine discovering the handler and validator? + +## Domain + +- Are invariants protected by the aggregate? +- Is the aggregate boundary based on lifecycle and consistency? +- Are foreign concepts referenced by IDs or translated facts? +- Is a domain service free of EF, Wolverine, and external clients? +- Does any domain event leave the module? + +## Integration Events + +- Is the event a completed fact? +- Is it published by the state owner? +- Is `Publish` used rather than `Invoke`? +- Does the publisher avoid assumptions about consumers? +- Does the contract avoid domain and infrastructure types? +- Is the consumer idempotent? +- Does the consumer mutate only local state? +- Is a local Command genuinely useful or merely ceremony? + +## Synchronous Cross-Module Queries + +- Does the consumer need a current answer, producer-owned semantics, or producer-owned authorization? +- Is the Query intentionally exposed under the producer's `Contracts.Queries`? +- Is the response narrow and transport-neutral? +- Does the handler read only producer-owned data? +- Does the dependency avoid cycles, fan-out, and nested Query chains? +- Is a Query invoked rather than published? +- Has any foreign Command, Command in Contracts, or Application-to-Application reference been introduced? + +## Local Reads + +- Does the query use only module-owned data? +- Are filtering, ordering, and paging performed in SQL? +- Is a separate projection table actually necessary? +- Is the response feature-local? + +## Cross-Module Read Models + +- Is the query in a dedicated application-level composition layer rather than inside one business module? +- Does the read use the default direct read-only cross-schema read model when no concrete requirement justifies an exception? +- When it departs from the default, does the decision address freshness and consistency, schema stability, sensitivity and authorization, change frequency, projection cost, or expected module separation? +- Does one `ApplicationReadDbContext` map every table participating in the LINQ query? +- Are mappings minimal read rows rather than module domain entities? +- Is the query no-tracking and projected in SQL with filtering, ordering, grouping, and paging before materialization? +- Does the read context avoid writes and migrations for module-owned source tables? +- If a persisted projection exists, does a separate projection-owned write context map and migrate only projection-owned tables? +- Are cross-schema dependencies and provider-sensitive behavior covered by SQL Server integration tests? +- Are directly read tables and columns acknowledged as persistence dependencies? +- Is a producer-owned SQL view defined and migrated by the producer? +- Does a sensitive detail read delegate current resource authorization to the owning module? +- If an event-fed projection is proposed, is it justified by performance, denormalization, independent storage, availability, scaling, analytics, search, or external integration? +- Why are a direct cross-schema read, producer-owned SQL view, and producer-owned Query insufficient? +- Has the projection's backfill and repair path been sketched, and is its complexity proportionate to the demonstrated benefit? +- If data is persisted, are eventual consistency, idempotency, failure handling, migrations, and backfills addressed? + +## Authorization + +- Does Identity provide identity facts only? +- Does the resource-owning module decide access? +- Does the code work outside HTTP? +- Is sensitive authorization based on current owner state rather than stale projection data? + +## Ordering + +- Is ordering infrastructure being introduced without a demonstrated need? +- If correctness depends on order, is the specific flow documented and tested? +- Could the consumer instead be idempotent or order-independent? + +## Verification + +- Domain tests updated? +- SQL Server integration tests updated? +- Wolverine pipeline tests updated? +- Cross-schema query tests updated, plus projection sequence tests when an event-fed projection exists? +- Architecture tests updated? +- Changed files, commands, and remaining risks reported? diff --git a/skills/dotnet/zerya-dotnet-modular-monolith/references/module-api.md b/skills/dotnet/zerya-dotnet-modular-monolith/references/module-api.md new file mode 100644 index 0000000..4c8a367 --- /dev/null +++ b/skills/dotnet/zerya-dotnet-modular-monolith/references/module-api.md @@ -0,0 +1,111 @@ +# Module Application API + +## Contents + +- [Purpose and Host-facing contract](#purpose) +- [Handler accessibility](#handler-accessibility) +- [Integration contract](#integration-contract) +- [Application ports](#application-ports) +- [Internal reuse](#internal-reuse) + +## Purpose + +`MOTOQ.Module..Application` is the Host-facing use-case assembly. + +Other modules do not reference it. + +## Supported Host-Facing Contract + +Public types may include: + +- Commands; +- Queries; +- response records; +- request-specific DTOs. + +Example: + +```csharp +public sealed record GetUserRoles(Guid UserId); + +public sealed record GetUserRolesResponse( + IReadOnlyList Roles); + +public sealed record RoleItem(Guid Id, string Name); +``` + +Do not expose: + +- EF entities; +- aggregate roots; +- `IQueryable`; +- Infrastructure options; +- handlers as callable APIs; +- domain events; +- broad service interfaces mirroring the module. + +## Handler Accessibility + +Handler classes are implementation details. + +Wolverine discovery and code generation may require public technical accessibility: + +```csharp +public static class GetUserRolesHandler +{ + public static Task> Handle(...) +} +``` + +Architecture tests must prevent: + +- another module referencing a handler; +- Host calling `Handler.Handle()` directly; +- public handler types being treated as supported contracts. + +Host invokes the message through Wolverine: + +```csharp +await bus.InvokeAsync>( + new GetUserRoles(userId), + cancellationToken); +``` + +## Integration Contract + +Cross-module contracts do not belong in Application. They belong in: + +```text +MOTOQ.Module..Contracts +``` + +Organize the assembly into `Queries` and `Events`. Allow only producer-owned Queries with transport-neutral responses and immutable Integration Events. Do not place Commands, handlers, validators, services, repositories, domain types, EF types, or Wolverine APIs in Contracts. + +Host-only Commands and Queries may remain in Application. Move a Query to Contracts only when another module or ReadModels intentionally consumes it. + +## Application Ports + +Declare an interface in Application when a use case needs a technical resource implemented by its own Infrastructure: + +```csharp +public interface IInvoiceExporter +{ + Task> Export( + ExportInvoiceModel invoice, + CancellationToken ct); +} +``` + +The port is owned by the consuming module and expressed in its language. + +Do not expose it to another module. + +## Internal Reuse + +For reuse inside one module: + +- call a domain method; +- call a named policy; +- call a feature-local application service. + +Do not redispatch through Wolverine merely to avoid a method call unless a distinct message boundary, transaction, retry, scheduling, or queue lifecycle is required. diff --git a/skills/dotnet/zerya-dotnet-modular-monolith/references/module-boundary-selection.md b/skills/dotnet/zerya-dotnet-modular-monolith/references/module-boundary-selection.md new file mode 100644 index 0000000..d0a61b3 --- /dev/null +++ b/skills/dotnet/zerya-dotnet-modular-monolith/references/module-boundary-selection.md @@ -0,0 +1,16 @@ +# Module Boundary Selection + +Confirm a capability boundary before creating projects or schemas. + +Strong evidence includes distinct business language and ownership, independently evolving rules, clear public operations and facts, isolated write transactions, meaningful access restrictions, or a credible future extraction or scaling need. + +Weak evidence includes a large class, many methods, a folder name, difficult EF migration, one broad handler, a single table group, or a separate UI area. + +Use: + +- a feature boundary for organization and ownership; +- a soft module when separate DbContexts, internal APIs, or architecture tests provide sufficient enforcement; +- a hard module when project references and public Contracts materially prevent recurring coupling. + +Before hardening, use `zerya-dotnet-ddd-architecture` to correct aggregate and transaction boundaries. Do not preserve a bad aggregate merely because it now fits inside a module. + diff --git a/skills/dotnet/zerya-dotnet-modular-monolith/references/module-integration.md b/skills/dotnet/zerya-dotnet-modular-monolith/references/module-integration.md new file mode 100644 index 0000000..83124ae --- /dev/null +++ b/skills/dotnet/zerya-dotnet-modular-monolith/references/module-integration.md @@ -0,0 +1,199 @@ +# Module Integration + +## Contents + +- [Integration model and contracts](#integration-model) +- [Public requests and synchronous queries](#public-requests) +- [Commands and integration events](#cross-module-commands) +- [Publish, invoke, and domain-event translation](#publish-versus-invoke) +- [Consumer ownership and local reactions](#consumer-ownership) +- [Failures, processes, and ordering](#failure-and-retry) + +## Integration Model + +The Zerya modular monolith uses this direction: + +```text +Host -> module synchronously through Command or Query +module -> module synchronously through a producer-owned Query when justified +module -> module asynchronously through an Integration Event +``` + +Use Wolverine for both in-process Query invocation and Event publication, but preserve their different semantics. Invocation is a synchronous dependency even when the API is asynchronous in C#. + +## Contracts Assembly + +Publish cross-module messages from `MOTOQ.Module..Contracts`: + +```text +Contracts/ + Queries/ + Events/ +``` + +Allow stable Query requests, Query responses, immutable Integration Events, and small contract-specific primitives. Include no Commands, handlers, validators, domain objects, EF types, services, repositories, or Wolverine APIs. + +## Public Requests + +Host may invoke a public module use case: + +```text +Host -> Invoke(ImportBankPayments) +Host -> Invoke(GetDocumentCase) +``` + +A module may invoke only a Query intentionally exposed in the producer's Contracts. It may not invoke a Host-facing Application request merely because its type is public. + +## Synchronous Cross-Module Query + +Use a producer-owned Query when the consumer needs a current answer, producer-owned interpretation, or producer-owned resource authorization: + +```csharp +var payment = await bus.InvokeAsync( + new GetPaymentSummary(paymentId), + cancellationToken); +``` + +Keep the Query narrow and transport-neutral. Let the producer handle it in its Application layer and read only producer-owned data. Record the synchronous dependency, prevent dependency cycles, and avoid fan-out or nested Query chains. + +Do not publish a Query. Do not describe `InvokeAsync` as asynchronous module integration; the caller waits for a response. + +## Cross-Module Commands + +Never invoke a foreign Command. A cross-module mutation obscures use-case ownership, transaction boundaries, and partial-failure behavior, and it cannot be expressed without breaking the Contracts or project-reference boundaries. Use a completed Event, a process manager, or a corrected module boundary. + +Do not create an ADR exception, place a Command in Contracts, or add an Application-to-Application reference. If an accepted repository source permits foreign Commands, stop and report that its target architecture conflicts with this skill. + +## Integration Events + +An Integration Event describes a fact that already happened. + +```csharp +public sealed record BankPaymentAcceptedIntegrationEvent( + Guid PaymentId, + Guid DocumentCaseId, + DateTimeOffset AcceptedAt); +``` + +The source module: + +- owns the state change; +- publishes the fact; +- does not know consumers; +- does not wait for consumer completion; +- does not expect a response. + +Use past-tense producer terminology. + +## Publish Versus Invoke + +```text +Invoke: + directed request + required handler + caller waits + cross-module Query semantics + +Publish: + completed fact + zero or many consumers + publisher does not wait + Integration Event semantics +``` + +Do not invoke an Integration Event merely to force synchronous projection updates. +Do not publish a Query or place a Command in Contracts. + +## Domain Event Translation + +Do not expose: + +```text +PaymentAcceptedDomainEvent +``` + +to another module. + +Translate: + +```text +Payment aggregate + -> PaymentAcceptedDomainEvent + -> Payments Application + -> BankPaymentAcceptedIntegrationEvent +``` + +This keeps the internal domain model private. + +## Consumer Ownership + +The consuming module translates the foreign fact into its own language. + +Example: + +```text +Payments publishes BankPaymentAccepted + +DocumentCases consumes it + -> loads its own DocumentCase + -> applies its own domain behavior + -> may publish a new DocumentCase fact +``` + +The consumer does not: + +- call Payments back synchronously unless it needs a justified Payments Query intentionally exposed in Contracts; +- mutate Payments data; +- reuse Payments domain entities; +- claim that Payments changed to a new state. + +## Local Command After an Event + +A consumer may introduce a local Command when it creates a useful local unit of work: + +```text +BankPaymentAccepted + -> local ReevaluateDocumentSettlement +``` + +This is optional. + +For a simple local reaction, the Integration Event handler may directly load its aggregate and call domain behavior. + +Do not introduce a local Command merely as ceremony. + +## Failure and Retry + +A consumer must be safe under repeated delivery. + +Use: + +- idempotent domain operations; +- idempotent projection upserts; +- stable event identity where effects are not naturally idempotent; +- durable Inbox and retry when appropriate. + +Do not turn an expected duplicate or already-applied fact into endless infrastructure retry. + +## Cross-Module Process + +If a business process spans modules: + +```text +PaymentAccepted + -> DocumentCases updates its state + -> DocumentMarkedAsPaid + -> another module reacts +``` + +Each module publishes the fact produced by its own committed state change. + +Do not create a synchronous chain of module Commands. Avoid Query chains where one module Query invokes another; compose them at the application read boundary or introduce a purpose-built read style instead. + +Use a process manager or saga only when the business process genuinely needs explicit progress, compensation, timeout, or completion tracking. + +## Ordering Caution + +Do not make global ordering guarantees part of the architecture by default. + +If one concrete consumer requires ordered handling of several event types, solve it locally with Wolverine configuration or an order-independent consumer design and cover it with tests. diff --git a/skills/dotnet/zerya-dotnet-modular-monolith/references/module-layout.md b/skills/dotnet/zerya-dotnet-modular-monolith/references/module-layout.md new file mode 100644 index 0000000..7ff637a --- /dev/null +++ b/skills/dotnet/zerya-dotnet-modular-monolith/references/module-layout.md @@ -0,0 +1,72 @@ +# Module Layout + +Consult this example only when creating or reorganizing a solution. Adapt capability names and test projects to the repository; do not create empty folders merely to mirror the tree. + +```text +backend/ +├── MOTOQ.Host/ +│ ├── Platform/ +│ ├── Http/ +│ └── Program.cs +├── MOTOQ.SharedKernel/ +├── Modules/ +│ ├── Identity/ +│ │ ├── MOTOQ.Module.Identity.Application/ +│ │ │ ├── Roles/ +│ │ │ │ ├── CreateRole.cs +│ │ │ │ ├── DeleteRole.cs +│ │ │ │ └── GetUserRoles.cs +│ │ │ ├── Users/ +│ │ │ ├── Groups/ +│ │ │ └── AssemblyMarker.cs +│ │ ├── MOTOQ.Module.Identity.Domain/ +│ │ │ ├── Roles/ +│ │ │ ├── Users/ +│ │ │ └── Groups/ +│ │ ├── MOTOQ.Module.Identity.Infrastructure/ +│ │ │ ├── Roles/ +│ │ │ ├── Users/ +│ │ │ ├── Groups/ +│ │ │ ├── Persistence/ +│ │ │ │ ├── IdentityDbContext.cs +│ │ │ │ └── Migrations/ +│ │ │ ├── ModuleRegistration.cs +│ │ │ └── AssemblyMarker.cs +│ │ ├── MOTOQ.Module.Identity.Contracts/ +│ │ │ ├── Queries/ +│ │ │ │ └── GetUserDisplayName.cs +│ │ │ └── Events/ +│ │ │ ├── UserDisabledIntegrationEvent.cs +│ │ │ └── UserDisplayNameChangedIntegrationEvent.cs +│ │ └── tests/ +│ │ ├── MOTOQ.Module.Identity.Domain.Tests/ +│ │ ├── MOTOQ.Module.Identity.Integration.Tests/ +│ │ └── MOTOQ.Module.Identity.Architecture.Tests/ +│ ├── DocumentCases/ +│ │ ├── MOTOQ.Module.DocumentCases.Application/ +│ │ ├── MOTOQ.Module.DocumentCases.Domain/ +│ │ ├── MOTOQ.Module.DocumentCases.Infrastructure/ +│ │ └── MOTOQ.Module.DocumentCases.Contracts/ +│ └── Payments/ +│ ├── MOTOQ.Module.Payments.Application/ +│ ├── MOTOQ.Module.Payments.Domain/ +│ ├── MOTOQ.Module.Payments.Infrastructure/ +│ └── MOTOQ.Module.Payments.Contracts/ +├── MOTOQ.ReadModels/ +│ ├── DocumentOverview/ +│ │ ├── GetDocumentOverview.cs +│ │ └── DocumentOverviewResponse.cs +│ ├── AccountingQueue/ +│ ├── Dashboard/ +│ ├── Search/ +│ ├── Persistence/ +│ │ ├── ApplicationReadDbContext.cs +│ │ └── Rows/ +│ └── ModuleRegistration.cs +└── tests/ + ├── MOTOQ.Architecture.Tests/ + ├── MOTOQ.ReadModels.Integration.Tests/ + └── MOTOQ.Host.Integration.Tests/ +``` + +`MOTOQ.ReadModels` is a read-composition layer, not a business module. It may map minimal read-only rows to tables in multiple module schemas. Add projection-owned tables and migrations only for a specifically justified persisted projection, and use a separate `ProjectionWriteDbContext` that maps only those tables. Keep `ApplicationReadDbContext` read-only and migration-free for module source tables. diff --git a/skills/dotnet/zerya-dotnet-modular-monolith/references/sources.md b/skills/dotnet/zerya-dotnet-modular-monolith/references/sources.md new file mode 100644 index 0000000..4b08ba4 --- /dev/null +++ b/skills/dotnet/zerya-dotnet-modular-monolith/references/sources.md @@ -0,0 +1,65 @@ +# Sources + +This skill is an original adaptation. It does not require copying a sample architecture literally. + +## Kamil Grzybek + +- Modular Monolith with DDD: + https://github.com/kgrzybek/modular-monolith-with-ddd + +- Modular Monolith: A Primer: + https://www.kamilgrzybek.com/blog/posts/modular-monolith-primer + +- Architectural Drivers: + https://www.kamilgrzybek.com/blog/posts/modular-monolith-architectural-drivers + +- Architecture Enforcement: + https://www.kamilgrzybek.com/blog/posts/modular-monolith-architecture-enforcement + +- Integration Styles: + https://www.kamilgrzybek.com/blog/posts/modular-monolith-integration-styles + +- Domain-Centric Design: + https://www.kamilgrzybek.com/blog/posts/modular-monolith-domain-centric-design + +Adapted concepts: + +- modules aligned to business capabilities; +- module encapsulation and internal-by-default design; +- module-owned data; +- explicit Host-to-module API; +- asynchronous module integration as the preferred style for completed facts; +- explicit trade-offs between synchronous Direct Calls and Messaging; +- separate Domain Events and Integration Events; +- outbox and inbox for reliable integration; +- compiler and architecture-test enforcement; +- explicit trade-offs between direct calls, shared-database reads, and messaging. + +Zerya-specific BYOI decisions: + +- one physical SQL Server database with a schema and write model per module; +- direct read-only cross-schema read models by default, with producer-owned SQL views, synchronous producer Queries, and event-fed projections as justified exceptions; +- event-fed persisted projections only as a last resort when concrete quality attributes outweigh delivery, monitoring, repair, migration, and backfill complexity and simpler cross-schema reads or producer Queries are insufficient. + +## Wolverine + +- Main documentation: + https://wolverinefx.io/ + +- EF Core transactional middleware and DbContext abstractions: + https://wolverinefx.io/guide/durability/efcore/transactional-middleware.html + +Relevant topics: + +- handler discovery and code generation; +- EF Core transactional middleware; +- transactional outbox and durable inbox; +- local and external messaging; +- retries and idempotency; +- modular-monolith handling patterns; +- testing and diagnostics. + +## Agent Skills + +- Specification: + https://agentskills.io/specification diff --git a/skills/dotnet/zerya-dotnet-persistence-evolution/SKILL.md b/skills/dotnet/zerya-dotnet-persistence-evolution/SKILL.md new file mode 100644 index 0000000..3017ab7 --- /dev/null +++ b/skills/dotnet/zerya-dotnet-persistence-evolution/SKILL.md @@ -0,0 +1,46 @@ +--- +name: zerya-dotnet-persistence-evolution +description: Design, implement, refactor, or review .NET persistence and data evolution with EF Core. Use when mapping DDD aggregates, separating write and read models, changing DbContexts or schemas, handling optimistic concurrency and transactions, planning expand-contract migrations, running backfills, removing legacy relationships, or diagnosing persistence coupling without redefining domain boundaries. +--- + +# Zerya .NET Persistence Evolution + +Implement an explicit domain and ownership decision without letting an EF graph, migration convenience, or query shape redefine it. If aggregate or Bounded Context ownership is unclear, use `zerya-dotnet-ddd-architecture` first. + +## Establish the persistence contract + +1. Read repository instructions, supported database providers, deployment topology, and accepted ownership decisions. +2. Trace affected writes, reads, mappings, constraints, migrations, jobs, imports, and repair paths. +3. Record the authoritative representation, transaction boundary, concurrency token, retention rules, and rollout constraints. +4. Separate domain-model changes from storage changes and query optimizations. +5. Define verification and forward-repair before moving material data. + +Read [aggregate-persistence.md](references/aggregate-persistence.md) for EF Core mappings and [transactions-and-concurrency.md](references/transactions-and-concurrency.md) when correctness depends on atomicity, retries, or conflicting writes. + +## Evolve data safely + +Prefer an observable expand-migrate-contract sequence: + +1. Expand the schema compatibly. +2. Deploy code that tolerates the transition state when staged rollout requires it. +3. Backfill deterministically in restartable batches. +4. Verify structural and domain-specific invariants. +5. Switch authoritative writes, then reads. +6. Tighten constraints after verification. +7. Remove obsolete compatibility code and schema in a later change. + +Read [migrations-and-backfills.md](references/migrations-and-backfills.md) before changing live data. Do not combine the first data move with destructive cleanup. + +## Keep reads independent from writes + +Project query shapes without inflating aggregate boundaries. Use a direct local query while it remains clear, secure, and operationally acceptable. Add a persisted projection only for demonstrated latency, scale, availability, search, analytics, or storage needs. + +Read [read-models.md](references/read-models.md) for query and projection decisions. + +## Route adjacent concerns + +- Use `zerya-dotnet-ddd-architecture` when persistence evidence reveals an uncertain aggregate, invariant, ownership, or Bounded Context. +- Use `zerya-wolverine-vertical-slices` when Wolverine owns transaction middleware, outbox, inbox, retries, or handler persistence. +- Use `zerya-dotnet-modular-monolith` only for already-justified module-owned schemas, DbContexts, or migration orchestration. + +For reviews, read [persistence-review.md](references/persistence-review.md). Report evidence, failure mode, rollout risk, smallest safe correction, and verification. Read [sources.md](references/sources.md) only for provenance or exact framework behavior. diff --git a/skills/dotnet/zerya-dotnet-persistence-evolution/agents/openai.yaml b/skills/dotnet/zerya-dotnet-persistence-evolution/agents/openai.yaml new file mode 100644 index 0000000..9ed8785 --- /dev/null +++ b/skills/dotnet/zerya-dotnet-persistence-evolution/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Zerya .NET Persistence Evolution" + short_description: "Evolve EF Core persistence safely" + default_prompt: "Use $zerya-dotnet-persistence-evolution to evolve this EF Core persistence design safely." diff --git a/skills/dotnet/zerya-dotnet-persistence-evolution/references/aggregate-persistence.md b/skills/dotnet/zerya-dotnet-persistence-evolution/references/aggregate-persistence.md new file mode 100644 index 0000000..ef55a18 --- /dev/null +++ b/skills/dotnet/zerya-dotnet-persistence-evolution/references/aggregate-persistence.md @@ -0,0 +1,15 @@ +# Aggregate Persistence + +Map storage around the chosen domain boundary. + +- Map an Aggregate Root and its internal entities as one change boundary. +- Use owned or complex types only when the value or dependent truly shares the root lifecycle and ownership semantics. +- Reference foreign aggregates by identifiers; avoid mutable cross-aggregate navigations and cascades. +- Prevent external code from mutating internal entity collections around the root. +- Keep persistence-only constructors, fields, and mapping accommodations from changing domain meaning. +- Load the state required for the command and invariant. Do not load unrelated collections merely because navigation properties exist. +- Treat the `DbContext` as a unit of work when that matches the application transaction boundary. + +Do not introduce a generic repository automatically. Add a repository abstraction when it expresses aggregate operations, protects model access, supports substitution required by the architecture, or prevents persistence concerns from leaking into Domain. Direct `DbContext` use in Application may be valid when repository ceremony adds no boundary or capability. + +Test mappings against the real provider when behavior depends on constraints, transactions, generated values, concurrency, collation, JSON, or relational semantics. EF Core's in-memory provider is not a substitute for relational integration tests. diff --git a/skills/dotnet/zerya-dotnet-persistence-evolution/references/migrations-and-backfills.md b/skills/dotnet/zerya-dotnet-persistence-evolution/references/migrations-and-backfills.md new file mode 100644 index 0000000..f908b4c --- /dev/null +++ b/skills/dotnet/zerya-dotnet-persistence-evolution/references/migrations-and-backfills.md @@ -0,0 +1,30 @@ +# Migrations and Backfills + +## Plan the transition + +Record: + +```text +Old and target representations: +Authoritative writes during each phase: +Compatibility window and deployment order: +Backfill key and batch size: +Restart checkpoint and idempotency rule: +Structural and domain verification: +Anomaly handling and quarantine: +Forward-repair or rollback strategy: +Cleanup gate: +``` + +## Expand, migrate, contract + +1. Add tables, columns, indexes, and nullable relationships without breaking old code. +2. Avoid expensive blocking operations in the same deployment unless their production impact is understood. +3. Make transitional reads and writes explicit. Use dual-write only for a bounded window, define failure semantics, and instrument mismatches. +4. Backfill in deterministic key order with bounded batches and durable checkpoints. +5. Record or quarantine malformed and ambiguous rows instead of guessing business meaning. +6. Verify counts, uniqueness, foreign keys, nullability, totals, state-machine rules, and sampled business scenarios. +7. Switch the authoritative writer before removing fallback reads. +8. Tighten constraints and remove legacy schema only after every supported application version no longer needs it. + +Prefer forward repair when rollback would need to reverse already accepted business writes. Test migrations on production-like volume and provider versions. Estimate lock duration, log growth, index build behavior, and operational observability. diff --git a/skills/dotnet/zerya-dotnet-persistence-evolution/references/persistence-review.md b/skills/dotnet/zerya-dotnet-persistence-evolution/references/persistence-review.md new file mode 100644 index 0000000..4791b40 --- /dev/null +++ b/skills/dotnet/zerya-dotnet-persistence-evolution/references/persistence-review.md @@ -0,0 +1,12 @@ +# Persistence Review + +1. Trace representative write and read paths to generated SQL and schema ownership. +2. Compare EF mappings with declared aggregate and lifecycle boundaries. +3. Identify cross-aggregate cascades, accidental navigations, oversized loads, missing constraints, tracking misuse, and broad concurrency tokens. +4. Review transaction, retry, outbox, and failure behavior together. +5. Review every migration for mixed application versions, locks, data anomalies, restartability, verification, and cleanup timing. +6. Review projections for freshness, idempotency, repair, authorization, and deletion. + +Classify findings as mapping mismatch, persistence coupling, transaction mismatch, concurrency defect, unsafe migration, unverifiable backfill, query inefficiency, read/write confusion, or projection-operability gap. + +For each material finding provide file and line evidence, affected data, realistic failure mode, smallest safe correction, rollout order, and verification. diff --git a/skills/dotnet/zerya-dotnet-persistence-evolution/references/read-models.md b/skills/dotnet/zerya-dotnet-persistence-evolution/references/read-models.md new file mode 100644 index 0000000..273d0f3 --- /dev/null +++ b/skills/dotnet/zerya-dotnet-persistence-evolution/references/read-models.md @@ -0,0 +1,27 @@ +# Read Models + +Separate write invariants from query shape. A screen joining several concepts does not imply one aggregate, write owner, or transaction. + +## Start with a direct query + +- Project only required columns to a response or read-model shape. +- Filter, order, group, and page before materialization. +- Use no tracking for read-only entity queries when its trade-offs are appropriate; projection to non-entity shapes usually avoids tracking naturally. +- Avoid exposing `IQueryable` across an ownership or application boundary. +- Apply authorization and tenant filtering at the authoritative boundary. +- Inspect generated SQL and query plans for material paths. + +Do not mandate `AsNoTracking` mechanically. Tracking queries can provide identity resolution and may be appropriate when results are later updated in the same unit of work. + +## Persist a projection deliberately + +Introduce a separate projection for demonstrated performance, availability, search, analytics, independent storage, or scale. Define: + +- freshness and user-visible consistency; +- event or change source; +- idempotency, ordering, and duplicate handling; +- replay, backfill, repair, and versioning; +- monitoring and reconciliation; +- authorization and deletion propagation. + +Eventual consistency is a cost of the selected design, not a requirement of CQRS. diff --git a/skills/dotnet/zerya-dotnet-persistence-evolution/references/sources.md b/skills/dotnet/zerya-dotnet-persistence-evolution/references/sources.md new file mode 100644 index 0000000..1f738f0 --- /dev/null +++ b/skills/dotnet/zerya-dotnet-persistence-evolution/references/sources.md @@ -0,0 +1,12 @@ +# Sources + +Use current provider and framework documentation for exact behavior. + +- [EF Core: transactions](https://learn.microsoft.com/en-us/ef/core/saving/transactions) — default `SaveChanges` atomicity, explicit transactions, savepoints, and execution-strategy caveats. +- [EF Core: concurrency conflicts](https://learn.microsoft.com/en-us/ef/core/saving/concurrency) — concurrency tokens and conflict resolution. +- [EF Core: efficient querying](https://learn.microsoft.com/en-us/ef/core/performance/efficient-querying) — projection, pagination, indexes, and related-data loading. +- [EF Core: tracking and no-tracking](https://learn.microsoft.com/en-us/ef/core/querying/tracking) — tracking behavior and identity-resolution trade-offs. +- [EF Core: applying migrations](https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/applying) — deployment approaches and production cautions. +- [Microsoft: infrastructure persistence layer](https://learn.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/infrastructure-persistence-layer-design) — aggregate repositories and CQRS persistence guidance; do not assume microservice deployment. + +Confirm behavior against the repository's EF Core version, provider, execution strategy, and production database. diff --git a/skills/dotnet/zerya-dotnet-persistence-evolution/references/transactions-and-concurrency.md b/skills/dotnet/zerya-dotnet-persistence-evolution/references/transactions-and-concurrency.md new file mode 100644 index 0000000..49da04f --- /dev/null +++ b/skills/dotnet/zerya-dotnet-persistence-evolution/references/transactions-and-concurrency.md @@ -0,0 +1,19 @@ +# Transactions and Concurrency + +## Choose the transaction boundary + +Align the transaction with the invariant being protected. A single EF Core `SaveChanges` is transactional for providers that support transactions; introduce explicit transaction control only when multiple saves, bulk operations, external coordination, or framework middleware requires it. + +Avoid distributed transactions by default. Use a transactional outbox when database state and outgoing messages must become durable together, then handle delivery idempotently. + +## Handle concurrent writes + +- Put concurrency tokens on the smallest write boundary whose decision depends on previously read state. +- Catch `DbUpdateConcurrencyException` at the application boundary. +- Decide deliberately whether to reject, reload and reevaluate, merge, or retry. +- Never retry a business command blindly after the facts used by its decision may have changed. +- Make retries safe for generated identifiers, side effects, message publication, and external calls. + +Test competing writes and verify the user-visible outcome, not only that EF throws an exception. + +When execution strategies or middleware manage retries and transactions, follow their composition rules. Do not nest manual transactions without checking provider and framework behavior. diff --git a/skills/dotnet/zerya-wolverine-vertical-slices/SKILL.md b/skills/dotnet/zerya-wolverine-vertical-slices/SKILL.md new file mode 100644 index 0000000..4d7bd92 --- /dev/null +++ b/skills/dotnet/zerya-wolverine-vertical-slices/SKILL.md @@ -0,0 +1,38 @@ +--- +name: zerya-wolverine-vertical-slices +description: Design, implement, refactor, or review .NET vertical slices built with Wolverine. Use for message and HTTP handlers, command and query pipelines, validation, EF Core transactional middleware, DbContext abstractions, cascading messages, transactional outbox or inbox, durable messaging, retries, scheduling, handler discovery, generated chains, or Wolverine-specific testing and diagnostics. +--- + +# Zerya Wolverine Vertical Slices + +Use Wolverine capabilities deliberately and verify behavior against the installed version and generated handler chains. Do not turn local project conventions into universal framework rules. + +## Inspect before changing + +1. Read repository instructions and existing slice conventions. +2. Identify the Wolverine, persistence-provider, and integration package versions. +3. Inspect bootstrap configuration, discovery policies, middleware, durability, routing, and storage registrations. +4. Trace the complete handler chain, transaction owner, outgoing messages, retries, and failure path. +5. Preserve accepted result, validation, and endpoint conventions unless the task changes them. + +Read [handlers-and-slices.md](references/handlers-and-slices.md) for slice design. Read [efcore-transactions-and-outbox.md](references/efcore-transactions-and-outbox.md) whenever a handler writes through EF Core or emits durable messages. + +## Build the slice + +- Give each message one clear intent and owner. +- Keep message-shape validation separate from domain invariants. +- Load only the state needed by the use case. +- Invoke domain behavior or an application process instead of encoding business meaning in pipeline plumbing. +- Make transaction, durability, idempotency, retry, and ordering requirements explicit. +- Return or publish messages according to Wolverine semantics verified for the installed version. +- Keep queries non-transactional unless they intentionally participate in a consistency operation. + +Read [messaging-and-integration.md](references/messaging-and-integration.md) for cascading messages, invoke versus publish, durable delivery, and consumers. Read [scheduling-and-diagnostics.md](references/scheduling-and-diagnostics.md) for delayed work, generated code, discovery, and routing diagnostics. + +## Respect adjacent boundaries + +- Use `zerya-dotnet-ddd-architecture` when a handler contains unresolved business rules, aggregate decisions, or domain events. +- Use `zerya-dotnet-persistence-evolution` for EF mappings, concurrency design, read models, schema changes, or backfills. +- Use `zerya-dotnet-modular-monolith` for ownership and contracts between accepted modules; Wolverine only implements that decision. + +For reviews, read [wolverine-review.md](references/wolverine-review.md). Report the observed configuration, generated behavior, consequence, correction, and verification command. Read [sources.md](references/sources.md) when exact APIs or version behavior matter. diff --git a/skills/dotnet/zerya-wolverine-vertical-slices/agents/openai.yaml b/skills/dotnet/zerya-wolverine-vertical-slices/agents/openai.yaml new file mode 100644 index 0000000..0982ce4 --- /dev/null +++ b/skills/dotnet/zerya-wolverine-vertical-slices/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Zerya Wolverine Vertical Slices" + short_description: "Build reliable Wolverine vertical slices" + default_prompt: "Use $zerya-wolverine-vertical-slices to implement or review these Wolverine vertical slices." diff --git a/skills/dotnet/zerya-wolverine-vertical-slices/references/efcore-transactions-and-outbox.md b/skills/dotnet/zerya-wolverine-vertical-slices/references/efcore-transactions-and-outbox.md new file mode 100644 index 0000000..ef29490 --- /dev/null +++ b/skills/dotnet/zerya-wolverine-vertical-slices/references/efcore-transactions-and-outbox.md @@ -0,0 +1,26 @@ +# EF Core Transactions and Outbox + +## Verify configuration + +Before relying on automatic persistence, confirm: + +- `WolverineFx.EntityFrameworkCore` and provider storage packages are installed; +- EF Core transactional middleware is registered; +- the handler chain is marked or auto-detected as transactional; +- Wolverine can identify exactly one transactional `DbContext` for the chain; +- any injected abstraction is registered through the Wolverine API supported by the installed version; +- durable message storage and outbox enrollment use the intended database and schema. + +Do not omit `SaveChangesAsync` merely because Wolverine is present. Omit it only when the generated transactional chain owns persistence. Do not call it manually inside that chain unless a documented use case requires an intermediate flush and its transaction semantics are understood. + +## Choose the transaction mode + +Use the configured default unless the use case requires another mode. Eager middleware opens an explicit transaction before the handler. Lightweight mode relies on `SaveChangesAsync` and is appropriate only when its documented limitations fit the operation. + +When a handler has multiple `DbContext`-shaped dependencies, designate the transactional context with the API or attribute supported by the installed Wolverine version. Treat other contexts as read-only unless an independently justified coordination mechanism exists. + +## Coordinate messages + +Use the transactional outbox when state changes and outgoing messages must become durable together. Verify that messages are persisted in the same database transaction as the chosen `DbContext`. Make consumers idempotent because durable delivery may repeat. + +Test the database write, outbox row, commit failure, retry, and eventual delivery with the real provider. diff --git a/skills/dotnet/zerya-wolverine-vertical-slices/references/handlers-and-slices.md b/skills/dotnet/zerya-wolverine-vertical-slices/references/handlers-and-slices.md new file mode 100644 index 0000000..21159e3 --- /dev/null +++ b/skills/dotnet/zerya-wolverine-vertical-slices/references/handlers-and-slices.md @@ -0,0 +1,22 @@ +# Handlers and Vertical Slices + +## Shape the message + +- Use a command for an intent to change state and a query for a requested answer. +- Name messages in the language of the use case, not after transport or CRUD plumbing. +- Keep contracts immutable and serializable when they may cross a durable boundary. +- Include correlation, tenant, actor, or idempotency data only when the message contract owns that requirement. + +## Keep the handler focused + +A handler may validate shape, authorize, load state, invoke domain behavior or an application process, map a result, and produce outgoing messages. Extract business conditions, state-machine transitions, fallback choices, and multi-step policies from the handler. + +Do not require a wrapper service merely to make a handler shorter. Add an application process when orchestration is reusable, independently testable, or too substantial for the slice to communicate clearly. + +## Apply validation at the right layer + +Use pipeline or request validation for required fields, basic formats, ranges, and mutually exclusive inputs. Keep invariants that must hold regardless of entry point in the domain model. Preserve the repository's validation and result library instead of assuming FluentValidation or a specific result type. + +## Design queries + +Project queries directly when no domain behavior is needed. Apply filtering, ordering, grouping, paging, tenant isolation, and authorization before materialization. Opt out of transactional middleware only after confirming how the repository applies it; an attribute is not necessary when no policy would add a transaction. diff --git a/skills/dotnet/zerya-wolverine-vertical-slices/references/messaging-and-integration.md b/skills/dotnet/zerya-wolverine-vertical-slices/references/messaging-and-integration.md new file mode 100644 index 0000000..70d4924 --- /dev/null +++ b/skills/dotnet/zerya-wolverine-vertical-slices/references/messaging-and-integration.md @@ -0,0 +1,22 @@ +# Messaging and Integration + +## Choose invoke, publish, send, or cascade + +- Invoke when the caller intentionally waits for one response and accepts a synchronous dependency. +- Publish when zero or more subscribers may react and absence of a subscriber is acceptable. +- Send when at least one subscriber is required and missing routing should fail. +- Return or yield cascading messages when the handler outcome naturally produces subsequent Wolverine messages and the installed version supports the selected form. + +Do not publish queries, invoke notifications merely to reuse a handler, or hide a synchronous call behind asynchronous vocabulary. + +## Define delivery semantics + +For durable messages define ownership, destination, durability, idempotency key, retry policy, poison-message handling, ordering needs, observability, and compatibility. Assume at-least-once effects unless the configured transport and consumer prove stronger semantics end to end. + +Use local queues for decoupled work within the application when durability and operational behavior are configured intentionally. Use external transports when process or deployment boundaries require them. + +## Handle failures + +Classify failures as transient infrastructure, expected business rejection, malformed message, optimistic conflict, or programmer defect. Configure retries only for failures likely to succeed later. Avoid retrying deterministic business rejections. Make side effects idempotent before enabling retry or replay. + +Keep integration contracts stable and translate from domain events or internal models rather than exposing aggregates directly. diff --git a/skills/dotnet/zerya-wolverine-vertical-slices/references/scheduling-and-diagnostics.md b/skills/dotnet/zerya-wolverine-vertical-slices/references/scheduling-and-diagnostics.md new file mode 100644 index 0000000..cdf3083 --- /dev/null +++ b/skills/dotnet/zerya-wolverine-vertical-slices/references/scheduling-and-diagnostics.md @@ -0,0 +1,30 @@ +# Scheduling and Diagnostics + +## Schedule work + +Use Wolverine's supported delayed or scheduled messaging API directly when scheduling is an application concern. Extract a domain policy when deciding whether or when work occurs is business behavior. + +Define timezone, clock source, duplicate scheduling, cancellation, missed execution, retry, and idempotency semantics. Use a durable mechanism when work must survive process failure. + +## Inspect generated behavior + +When discovery, middleware, transactions, storage, routing, or code generation changes, inspect the generated chains instead of inferring behavior from handler source alone. + +Typical repository verification may include: + +```bash +dotnet run -- describe +dotnet run -- codegen write +``` + +Use the actual startup project and commands supported by the installed Wolverine version. Review generated code or diagnostics for: + +- selected handler and dependencies; +- middleware order; +- chosen transactional storage; +- entity loading and persistence; +- cascading or published messages; +- endpoint or transport route; +- retry and durability policies. + +Add focused integration tests for behavior that depends on generated chains. Do not snapshot large generated outputs when a smaller behavioral assertion is stable. diff --git a/skills/dotnet/zerya-wolverine-vertical-slices/references/sources.md b/skills/dotnet/zerya-wolverine-vertical-slices/references/sources.md new file mode 100644 index 0000000..30721ae --- /dev/null +++ b/skills/dotnet/zerya-wolverine-vertical-slices/references/sources.md @@ -0,0 +1,13 @@ +# Sources + +Use documentation matching the installed Wolverine major and minor version. + +- [Wolverine: handlers](https://wolverinefx.net/guide/handlers/) — discovery, method signatures, cascading messages, middleware, and generated code. +- [Wolverine: EF Core integration](https://wolverinefx.net/guide/durability/efcore/) — package setup, DbContexts, outbox, and provider integration. +- [Wolverine: transactional middleware](https://wolverinefx.net/guide/durability/efcore/transactional-middleware.html) — transaction modes, auto-application, abstractions, and transactional-context selection. +- [Wolverine: durable messaging](https://wolverinefx.net/guide/durability/) — inbox, outbox, retries, dead letters, and durable local queues. +- [Wolverine: sending messages](https://wolverinefx.net/guide/messaging/message-bus) — invoke, send, publish, delivery options, and scheduled delivery. +- [Wolverine: message routing](https://wolverinefx.net/guide/messaging/subscriptions) — subscriptions and local or external routing. +- [Wolverine: command-line diagnostics](https://wolverinefx.net/tutorials/command-line-diagnostics) — `describe`, focused routing and discovery diagnostics, code generation, and storage inspection. + +Framework behavior evolves. Inspect package versions and generated chains before applying an API or convention from these references. diff --git a/skills/dotnet/zerya-wolverine-vertical-slices/references/wolverine-review.md b/skills/dotnet/zerya-wolverine-vertical-slices/references/wolverine-review.md new file mode 100644 index 0000000..1fe3cd0 --- /dev/null +++ b/skills/dotnet/zerya-wolverine-vertical-slices/references/wolverine-review.md @@ -0,0 +1,13 @@ +# Wolverine Review + +1. Record package versions and relevant bootstrap configuration. +2. Trace representative command, query, consumer, and scheduled-message chains. +3. Inspect generated middleware and routing diagnostics. +4. Verify the transactional `DbContext`, save behavior, outbox enrollment, and commit ordering. +5. Verify invoke, publish, send, and cascading-message semantics against caller intent. +6. Test retries, duplicates, poison messages, shutdown, and replay where material. +7. Distinguish framework defects from domain-model, persistence, and module-boundary problems. + +Classify findings as discovery mismatch, middleware mismatch, ambiguous transaction owner, broken outbox atomicity, incorrect messaging semantic, unsafe retry, missing idempotency, routing error, scheduling durability gap, or unnecessary framework abstraction. + +For each material finding provide file and line evidence, observed generated behavior, realistic consequence, smallest correction, and an exact verification method.