| description | Project-wide guidelines and conventions for CoreEx development | |||
|---|---|---|---|---|
| tags |
|
CoreEx is a modular .NET framework for enterprise APIs and distributed services. Favor CoreEx-native primitives, patterns, and extensions over ad-hoc implementations.
A CoreEx solution scaffolded with dotnet new coreex records its project-wide choices in the solution-root
AGENTS.md "Feature Configuration" block: data-provider (SQL Server / PostgreSQL / None), refdata-enabled,
rop-enabled (exception vs Result<T>), outbox-enabled, and messaging-provider. Whether a Domain layer is
present is inferred from the existence of src/*.Domain/ (added via dotnet new coreex-domain).
Before asking the user a project-wide question, resolve it from that recording first, cross-checked against the
real artefacts (package references, dbex.yaml, presence of the *.Domain project). Re-state the resolved values for
confirmation rather than re-prompting; if the recording and artefacts disagree, stop and flag rather than guessing.
When a change enables a feature the recording marks disabled (e.g. the first ref-data type in a refdata-enabled: false solution), update the recording. The skills and instructions rely on this — it is what keeps them from
re-asking data-provider/rop-enabled/etc. on every task. (In this framework repository there is no such generated
solution; the rule applies to consumer solutions where these assets are installed.)
CoreEx.sln: main solution for framework + samples.src\: reusable CoreEx libraries (AspNetCore, Database, EntityFrameworkCore, Events, Validation, DomainDriven, RefData, Caching, etc.).gen\CoreEx.Generator\: Roslyn source generator for contracts.tests\: framework-level tests.samples\src\Contoso.*\: sample domains split by layer/host.samples\aspire\AppHost.cs: orchestration entrypoint.coreex-starter\: separate starter template repo — ignore unless user wants starter changes.
- Build:
dotnet build CoreEx.sln - Test:
dotnet test CoreEx.slnor target specific projects. - Single test:
dotnet test <proj> --filter "FullyQualifiedName~<name>" - Samples: docker-compose infrastructure + dotnet run for Database projects + Aspire AppHost.
- Linting: No separate
dotnet format. Build is the lint pass (nullable, LangVersion=preview, TreatWarningsAsErrors insrc\Directory.Build.props). - Formatting: 4 spaces for
*.cs, 2 spaces for*.json|*.xml|*.yaml|*.props|*.csproj|*.sln|*.sqlper.editorconfig.
All sample hosts depend on containerised infrastructure. Start it before running any host or integration test:
podman compose -f docker-compose.yml up -d # Podman preferred; `docker compose` also works| Service | Port(s) | Purpose |
|---|---|---|
db-sql-server |
1433 | Shopping domain database; Service Bus emulator backing store |
db-postgres |
5432 | Products domain database |
redis-cache |
6379 | FusionCache Redis backplane (all domains) |
servicebus-emulator |
5672 AMQP, 5300 mgmt | Azure Service Bus emulator; namespace sbemulatorns; topic contoso with subscriptions products and shopping (both session-enabled); config at servicebus/Config.json |
dts-emulator |
8080, 8082 | Azure Durable Task Scheduler emulator; task hubs default and order |
aspire-dashboard |
18888 UI, 4317 OTLP | Standalone OpenTelemetry dashboard; usable without running the full Aspire AppHost |
Connection strings for each service in development are in each host's appsettings.Development.json under the Aspire: configuration key hierarchy. See samples/docs/local-dev.md for full detail, connection string patterns, and startup sequences.
- Two roles: framework packages (
src\) + sample reference implementations (samples\). - Business layers (strict inward dependency — inner layers have no knowledge of outer):
*.Contracts→*.Domain(optional) →*.Application→*.Infrastructure. Confirmed by project references:*.Domainreferences only*.Contracts;*.Applicationreferences*.Contractsand*.Domain;*.Infrastructurereferences*.Application(and transitively*.Domain) — never the reverse in either case. - Host layers (composition roots, no business logic):
*.Api,*.Relay,*.Subscribe. - Design-time tooling (no runtime presence):
*.CodeGen(generates reference-data layer fromref-data.yaml) and*.Database(schema, seeding, outbox infrastructure via DbEx). - Sample flow: Controllers →
WebApihelpers → Application services (validate +IUnitOfWork) → Infrastructure repositories (EF + explicit mappers) → transactional outbox → relay publishes to Service Bus → subscribers consume. - Polyglot data: Products uses PostgreSQL (
CoreEx.Database.Postgres+CoreEx.EntityFrameworkCore); Shopping uses SQL Server (CoreEx.Database.SqlServer+CoreEx.EntityFrameworkCore). Layers above Infrastructure are database-agnostic. - Primary domains: Products and Shopping complete; Orders WIP. See
samples\README.mdfor topology. - Aspire: orchestrates all sample hosts in
samples\aspire\Contoso.Aspire\AppHost.csfor local distributed development and E2E testing.
- Prefer CoreEx primitives before introducing external libraries that overlap with framework capabilities.
- Prefer CoreEx exception types (
NotFoundException,ValidationException,BusinessException,ConcurrencyException, etc.) and CoreExResult/Result<T>flows over custom error wrappers. - Do not introduce AutoMapper in any repository unless the repository maintainer explicitly requests it. Repositories and services use explicit mapping helpers/classes.
- Contracts are commonly declared as
[Contract] public partial class .... - Mutable contracts often implement
IIdentifier<T>,IETag, andIChangeLog. - Use
[ReadOnly(true)]for server-managed fields and[ReferenceData<T>]for reference-data-backed code properties. - Canonical casing transformations belong in property setters when already established by the model (for example
Skuuppercasing inProductBase). - Favor the existing source-generation approach; do not hand-write members that are meant to be generated.
- Services and repositories commonly self-register with
[ScopedService<...>]. - Hosts use
AddDynamicServicesUsing<T1, T2, ...>()to discover and register services instead of manually wiring every type. - Keep interface/implementation layering intact:
- application interfaces live in
Application\Interfaces\,Application\Repositories\,Application\Adapters\, orApplication\Policies\; - infrastructure implementations live in
Infrastructure\.
- application interfaces live in
- There are two distinct mapping layers — do not conflate them:
- Application-level (
Application\Mapping\): Domain aggregate → Contract, usingMapper<TSource, TDest, TSelf>; present only in domains with a Domain layer (e.g. Shopping). - Infrastructure-level (
Infrastructure\Mapping\): Contract ↔ Persistence model, usingBiDirectionMapper<TFrom, TTo, TSelf>; present in all domains.
- Application-level (
- Application services follow a repeated pattern:
- guard/normalize inputs;
- validate with CoreEx validators;
- load current state where needed;
- wrap mutations and event publication together inside
_unitOfWork.TransactionAsync(...)— both the database write and the outbox event are committed atomically or not at all; - add
EventDatato_unitOfWork.Eventsinside that same transactional scope.
- Use exception-based flows for straightforward CRUD-style services.
- Use
Result<T>pipelines for aggregate-oriented flows and multi-step orchestration, especially in Shopping.
- When a domain needs to interact with another domain or external service, define an adapter interface in
Application\Adapters\. The Application layer depends on this domain-idiomatic abstraction — never on the remote API's schema or transport directly. - Infrastructure implements the adapter using a typed HTTP client (
Infrastructure\Clients\) for the transport concern, keeping client and orchestration in separate focused classes. - Two adapter roles appear in Shopping:
- Synchronous adapter (
IProductAdapter) — real-time calls (e.g. inventory reservation at checkout); the HTTP client is called live inside the unit of work. - Sync/replication adapter (
IProductSyncAdapter) — event-driven data replication; receives published domain events and maintains a local eventually-consistent copy in the domain's own store.
- Synchronous adapter (
- Do not call
HttpClientdirectly from services — always go through the adapter interface.
- Policies (
Application\Policies\) encapsulate domain-level guard logic that requires I/O — adapter or repository calls. A policy provides a named, independently testable home for rules that depend on external state and cannot be expressed in a validator alone (synchronous) or in the domain model (no async I/O). Policies returnResultorResult<T>and can be called from any point in service orchestration where the condition needs to be verified. - Use a policy when an invariant cannot be expressed in a validator alone (e.g. confirming a referenced entity exists before allowing a mutation).
- Policies return
ResultorResult<T>and compose naturally intoResult<T>service pipelines via.GoAsync()/.ThenAsAsync().
Program.csfiles follow a predictable CoreEx host shape:builder.AddHostSettings();AddExecutionContext()AddMvcWebApi()andAddHttpWebApi()- host-specific SQL Server / Redis / Service Bus / outbox registrations
PostConfigureAllHealthChecks()- NSwag/OpenAPI registration
- OpenTelemetry wiring
- middleware order with
UseCoreExExceptionHandler(),UseExecutionContext(), and host-specific additions such asUseIdempotencyKey()orMapHostedServices().
- API hosts, subscriber hosts, and outbox relay hosts intentionally have different startup shapes. Do not collapse them into one generic startup unless the user explicitly asks for that refactor.
- Use CoreEx
WebApihelpers (PostAsync,PutAsync,PatchAsync,DeleteAsync). - PATCH:
application/merge-patch+json. - POST: use
[IdempotencyKey]. - OpenAPI/health endpoints standard in hosts.
- Transactional outbox + Azure Service Bus are first-class messaging patterns across all domains.
- Products uses PostgreSQL; Shopping uses SQL Server. Do not assume SQL Server when working on Products.
- Shopping: synchronous HTTP inventory reservation + transactional outbox + async event publishing. Preserve this split.
- Both domains use
CoreEx.Caching.FusionCache(hybrid in-process + Redis backplane cache) for reference data and idempotency. Register viaAddFusionCache()/AddFusionHybridCache()inProgram.cs; clear viaTest.ClearFusionCacheAsync()in test[OneTimeSetUp].
- Framework: UnitTestEx + NUnit + AwesomeAssertions (the
AwesomeAssertionsNuGet package — not FluentAssertions). - Sample:
WithGenericTester<EntryPoint>(unit) orWithApiTester<Program>(API/Subscribe/Relay). - Integration tests: per-class named seed files
Data\read-data.seed.yaml/Data\mutate-data.seed.yaml(Products), or a singleData\data.yaml(Orders/Shopping), in Test.Common +Resources\JSON expectations. - Intra-domain dependencies are real; inter-domain dependencies are always mocked. Own database, cache, and outbox are started and seeded in
[OneTimeSetUp]. Cross-domain HTTP calls and direct broker publishes are replaced withMockHttpClientFactory/UseExpectedAzureServiceBusPublisher(). - Outbox assertion helpers are database-specific:
UseExpectedPostgresOutboxPublisher()for Products;UseExpectedSqlServerOutboxPublisher()for Shopping. Do not use the SQL Server helper in Products tests. - Mock downstream HTTP calls; do not assume live APIs.
- Code comments end with a period/full stop.
- Always use
.ConfigureAwait(false)in service/repository code. <Nullable>enable</Nullable>and<ImplicitUsings>enable</ImplicitUsings>are set inDirectory.Build.props— treat nullable warnings as errors, never suppress them with!without justification.- Every project has a single
GlobalUsing.csat the project root. Allusingstatements go there — never in individual source files. The code generator (*.CodeGen) emits nousingstatements and depends on this. - File-scoped namespace declarations only:
namespace Foo.Bar;— never block-scoped. - Single-line
ifbodies do not need braces:if (x) return; - Use expression-bodied syntax (
=>) when the entire method or property body is a single expression. - Private instance fields are always prefixed with
_.
Never create or edit *.g.cs, *.g.sql, or *.g.pgsql files directly. Each generator owns its outputs:
| File pattern | Generator | Change instead |
|---|---|---|
*.g.cs (contracts, ref-data) |
Roslyn source generator (CoreEx.Generator) |
The [Contract]- or [ReferenceData]-decorated partial class |
*.g.cs (ref-data layer — controller, service, repository, mapper) |
*.CodeGen project (CoreEx.CodeGen + ref-data.yaml) |
ref-data.yaml config or the Handlebars templates in CoreEx.CodeGen/RefData/Templates/ |
*.g.sql, *.g.pgsql, *DbContext.g.cs, Persistence/*.g.cs |
*.Database project (DbEx) |
DbEx YAML config or SQL migration scripts |
See INSTRUCTION_AUTHORING.md for full generator ownership detail.
README.md— repo-level positioning and top-level commands.samples\README.md— runnable Contoso architecture and local setup.docs\capabilities.md— deeper CoreEx capability and pattern explanations.samples\docs\layers.md— full layer diagram, dependency rules, and design-time tooling overview.samples\docs\patterns.md— pattern catalog with links to layer-specific detail for every architectural, application, messaging, and testing pattern used in the samples.samples\docs\<layer>.md— detailed walkthrough for each layer:contracts-layer.md,application-layer.md,domain-layer.md,infrastructure-layer.md,hosts-layer.md,testing.md,tooling.md..github\instructions\*.instructions.md— area-specific rules auto-injected when editing matching files (Program.cs, contracts, application services, repositories, validators, subscribers, tests).
The following prompts, skills, and templates are available. Type / in chat to invoke prompts and skills; use
dotnet new in a terminal for templates. For the full catalog and the skills-vs-prompts-vs-instructions distinction,
see coreex-ai-workflows.md.
| Command | Type | When to use |
|---|---|---|
CoreEx.Template |
Template pack | Deterministic dotnet new scaffolding. Pin the version — dotnet new install CoreEx.Template::<version> — then dotnet new coreex (solution), coreex-api / coreex-relay / coreex-subscribe (hosts), or coreex-ai (AI workflow assets). |
CoreEx Expert |
Agent | Architecture guidance, pattern recommendations, and design review. Invoke via /coreex-expert (or @coreex-expert). |
/coreex-scaffold |
Skill-backed prompt | Guided greenfield solution scaffolding (chooses the smallest safe shape, runs the dotnet new coreex* commands). |
/coreex-docs-sync |
Skill | Refresh the whole AI asset bundle (instructions, skills, prompts, the coreex-expert agent, and the .github/docs/coreex/ doc cache) to a new pinned CoreEx version after a version bump. |
/coreex-<capability> |
Skills (L1) + matching prompts | Add or modify one building block: coreex-contract, coreex-refdata, coreex-db-migration, coreex-repository, coreex-adapter, coreex-app-service, coreex-validator, coreex-policy, coreex-aggregate, coreex-api, coreex-subscriber, and coreex-test-api / coreex-test-subscribe / coreex-test-relay. Each skill has a .prompt.md wrapper for Copilot. |
/acquire-codebase-knowledge, /aspire |
Skills | Repo onboarding documentation; local Aspire orchestration. |
When creating or maintaining Copilot instruction files and skills:
- Instruction files (
.instructions.md) — see INSTRUCTION_AUTHORING.md for standards on YAML frontmatter, section order, and content rules. - Skill files (
SKILL.md) — see SKILL_AUTHORING.md for the directory structure pattern (references/,assets/), lean main file rules (<300 lines), and cross-referencing guidelines.
Both documents define durable patterns for creating guidance that is discoverable, maintainable, and context-efficient.