Search/Build Domain-Driven (DDD) .NET Modules Without Breaking Layers or Public APIs

Build Domain-Driven (DDD) .NET Modules Without Breaking Layers or Public APIs

Use when an AI changes a modular .NET app built with domain-driven design. It follows existing patterns, keeps layer dependencies one-way, puts aggregates, repositories, and DTOs in the right layers, and tests on every supported database without breaking current users.

To use this AI rule
## Reuse Existing Patterns and Keep Layer Dependencies One-Way Search the repository for a comparable implementation before creating code. Follow established project structure, naming, dependency, testing, and extension patterns unless the requested change explicitly replaces them. Keep diffs focused; avoid incidental formatting, refactoring, or dependency upgrades. Use explicit layers with one-way dependencies: - **Domain.Shared:** constants, enums, localization resources, event contracts, and shared types that are safe for every layer and external clients. - **Domain:** entities, aggregate roots, repository contracts, domain services, and business rules. - **Application.Contracts:** application-service interfaces and data-transfer objects. - **Application:** application-service implementations and use-case orchestration. - **Persistence integrations:** provider-specific contexts, mappings, and repository implementations that depend on the domain. - **HTTP API:** transport adapters that depend on application contracts, not implementations. - **Remote client:** generated or handwritten proxies that depend on application contracts. - **Web UI:** presentation code that consumes the HTTP API. Keep web, transport, authentication-context, and ORM concerns out of the domain. Keep application logic independent of the web layer. Treat a forbidden reference as an architectural defect, even when it would make one implementation shorter. Preserve public APIs and package boundaries. Do not introduce a breaking change unless it is explicitly requested, justified, migrated, and covered by compatibility tests. ## Keep Aggregates Valid From Creation and Move Cross-Entity Rules to Domain Services Create entities in the domain layer and make them valid at construction. The primary constructor should enforce invariants and initialize every owned collection. When an ORM requires it, provide a protected parameterless constructor without weakening normal creation rules. Accept an identifier from the caller instead of generating it inside the entity. Protect consistency with non-public setters and intention-revealing domain methods. Make members overridable where the runtime, proxy system, or supported extension model requires it. Aggregate roots should use one stable identifier rather than a composite key, remain small, and reference other aggregate roots by identifier rather than navigation property. Put cross-entity rules that belong to the model in a domain service. Create an interface only when multiple implementations, replacement, or test isolation genuinely requires one. Use a clear domain-oriented suffix such as `Manager`, accept domain objects and primitives rather than DTOs, and throw localized business exceptions with unique namespaced error codes when rules fail. Domain services may perform domain-specific lookup, normalization, caching, or resolution, but simple persistence queries belong in repositories. Do not read the current authenticated user inside the domain; pass the required actor or tenant values from the application layer. ## Create One Repository per Aggregate Root, Independent of the Database Define repository contracts in the domain layer, with one dedicated contract per aggregate root. Do not create repositories for internal entities that are owned by an aggregate. Keep public repository contracts provider-independent. Prefer basic or read-only repository capabilities and do not expose a query-provider abstraction such as `IQueryable` to module consumers. Provider implementations may use richer query APIs internally. Make every repository operation asynchronous and accept an optional cancellation token. For a method returning one aggregate, default detail loading to enabled when callers normally need a complete aggregate. For list methods, default it to disabled to avoid unbounded joins and data transfer. Use meaningful aggregate-returning methods instead of returning ad hoc composite projection classes. Add projection-specific methods only when measured performance requirements justify them. Keep eager-loading logic in a provider-specific `IncludeDetails` helper so application services do not know how a store assembles an aggregate. Pass cancellation tokens through the framework's normalization helper when one exists. Ensure provider-level data filters, tenancy, and soft-delete rules remain active when obtaining a queryable internally. ## Design Application Service Interfaces With a Separate DTO per Operation Place one interface per application service in the contracts package. Use DTOs and primitives at the boundary; never accept or return domain entities. Keep methods asynchronous, use a consistent `Async` suffix, and avoid repeating the entity name when the service context already makes it clear. Use predictable CRUD shapes: identifier arguments stay separate from update DTOs, create and update operations return a detailed DTO when useful, deletes return no payload, and list queries hold optional filtering, sorting, and paging fields with a safe maximum page size. Create a separate input DTO for each operation. Do not reuse input DTOs merely because fields currently match, and avoid inheritance between input DTOs. Keep DTOs free of business logic, validate them at the boundary, and reuse shared constants for limits. Prefer a basic output DTO plus a detailed variant over many overlapping shapes. Application implementations orchestrate use cases. They should use dedicated repositories, load required aggregates, call domain methods, and explicitly persist updates rather than assuming change tracking. Keep data queries out of application services. Map extensible properties through the supported mapper convention. Do not pass web types, upload abstractions, or streams through application contracts. Let transport adapters convert files into a stable application-level representation. Avoid calling another application service in the same module; move reusable rules into the domain or a focused shared helper. Cross-module calls must go through the other module's public contracts. Make public operations and intended extension helpers overridable. Prefer protected extension seams over private helpers when customization is part of the framework's contract. ## Separate Database Code per Provider and Keep Each Module Standalone Give each module a separate context interface and implementation for each supported persistence provider. Expose sets or collections only for aggregate roots. Attach the module's connection name explicitly, keep table or collection prefixes configurable, and reserve framework-owned prefixes. Do not rely on lazy loading. Configure mappings in a named model-builder extension rather than filling the context lifecycle method with entity details. Apply the framework's conventional mapping to every entity before module-specific rules. Provider-specific repository implementations should inherit the provider's standard base, use the context interface as their boundary, pass cancellation tokens correctly, and implement aggregate detail loading in the provider-appropriate way. A document store may naturally load owned collections, while a relational provider needs explicit includes; neither distinction should leak into the domain or application contract. Each package should expose one module-composition class with explicit dependency declarations. Register services and options during configuration; use application-initialization hooks only for work that truly requires the running application. A module must remain usable on its own without hidden coupling to another package. When a module supports multiple providers, run the same repository and behavior contract against each integration. Provider independence is a tested property, not a naming convention. ## Add Extension Points and Deprecate Old APIs Instead of Breaking Them Design public and protected members for the framework's supported extension model. Use overridable methods, replaceable service registrations, interfaces at genuine substitution points, and options objects for configuration. Document extension points and the invariants custom implementations must preserve. Do not remove or rename a public member without a deprecation cycle. Mark the old member obsolete with clear migration guidance, add optional parameters with defaults, and provide default implementations for new base-class members whenever possible. Prefer adding a new interface over breaking an existing one. Keep user-facing strings in localization resources owned by the shared domain package. Use namespaced keys and localized business errors rather than hardcoded text. Define setting, feature, and permission names as constants in their owning layer; organize them under stable module namespaces. Use an in-process event bus for communication inside one process and a distributed event bus across modules or services. Place distributed event contracts in the shared domain package, keep them simple and serializable, and use primitives or nested event contracts rather than domain entities. Handle integration events in the application layer. Treat compatibility, localization, and extension behavior as public product surfaces. Cover changes to them with focused tests and migration notes. ## Test Each Change on Every Supported Database and Check the Final Diff Before a significant architectural or public-contract change, align through the project's approved design or issue process. Keep the pull request scoped and reviewable, and include the tests needed to prove the affected behavior. Use unit tests for isolated domain rules and integration tests for repositories, mappings, permissions, validation, and module composition. Exercise every supported persistence provider when the contract claims provider independence. Name tests after the operation, scenario, and expected result. Review the final diff against this checklist: - Dependency direction is intact, with no web or persistence concerns leaking inward. - Entities enforce invariants, owned collections are initialized, and aggregate references use identifiers. - Repository contracts remain asynchronous, cancellable, aggregate-focused, and provider-independent. - Application contracts use minimal validated DTOs and stable method shapes. - Application services delegate queries to repositories and mutations to domain methods. - Relational and document integrations follow their own mapping and detail-loading conventions. - Public APIs, localization keys, settings, features, permissions, and events remain compatible. - Tests cover permissions, validation, failure cases, provider differences, and edge conditions. - The diff contains no unrelated refactor, generated-file edit, or unnecessary API surface. Report the focused checks that ran and any provider or compatibility path that remains unverified. Do not claim architectural compliance from compilation alone.