onion-architecture
>
Install
mkdir -p .claude/skills/onion-architecture && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/15309" && unzip -o skill.zip -d .claude/skills/onion-architecture && rm skill.zipInstalls to .claude/skills/onion-architecture
Activation
This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.
Explains and enforces the Onion (Ports & Adapters) architecture used in PredictiveAnalyticsManager. Use this skill whenever the user asks where to put new code, which layer something belongs to, why a dependency is not allowed, how to avoid coupling between layers, what base classes to extend, how to structure a new feature folder, or any question about separation of concerns — even if they don't use the word "architecture". Also reference this skill proactively when reviewing code that may be putting logic in the wrong layer (e.g. business rules in a controller, DB queries in a use case, domain logic in infrastructure).About this skill
Onion Architecture Guide
PAM is structured as four concentric layers. The fundamental rule is dependencies only point inward: outer layers may reference inner layers, but inner layers must never reference outer ones.
┌─────────────────────────────────────────────────────────────────┐
│ API / Presentation (Controllers, Validators, DI wiring) │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ Infrastructure (Repositories, DB models, AWS clients) │ │
│ │ ┌─────────────────────────────────────────────────────┐ │ │
│ │ │ Application (Use cases, Ports/interfaces, DTOs) │ │ │
│ │ │ ┌───────────────────────────────────────────────┐ │ │ │
│ │ │ │ Domain (Entities, business rules, enums) │ │ │ │
│ │ │ └───────────────────────────────────────────────┘ │ │ │
│ │ └─────────────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
These rules are enforced automatically by the ArchUnit tests in PredictiveAnalyticsManager.ArchUnitTests.
If you violate a rule, a test will fail — treat it as a hard constraint, not a style suggestion.
Layer 1: Domain
Assembly: PredictiveAnalyticsManager.Domain
Namespace prefix: Prophet.SaaS.PredictiveAnalyticsManager.Domain
This is the innermost layer and has no dependencies on any other PAM assembly. It is the source of truth for what the system's concepts are and what rules govern them.
What belongs here
- Entities — classes extending
BaseModel(full entity with audit fields) or referencingBaseLiteModel(lightweight projection for list queries). - Business rules and invariants — validation that is part of the domain's identity, e.g. "a project name must be unique", "a model cannot be activated while in draft status". These live inside entity methods or companion classes, not in use cases or controllers.
- Aggregate guard classes — a plural companion class (e.g.
Projects.cs) that enforces cross-entity rules within the same aggregate. - Enums — status, type, or category enums that describe domain concepts.
- Domain exceptions — thrown when a domain invariant is violated.
Base classes to extend
| Class | Use when |
|---|---|
BaseModel | Full entity with Id, CreatedBy, CreatedAt, ModifiedBy, ModifiedAt |
BaseLiteModel | Lightweight read projection for list/lite queries |
What does NOT belong here
- Any reference to
IServiceCollection, AutoMapper, or any framework type - Repository calls or data access of any kind
- DTO types
Feature folder layout
Domain/
<Feature>/
Feature.cs ← Entity (extends BaseModel)
FeatureLite.cs ← Optional lite projection (extends BaseLiteModel)
Features.cs ← Aggregate guard (optional, for cross-entity rules)
FeatureStatus.cs ← Enum
Checklist
- Entity inherits from
BaseModelorBaseLiteModel - All invariants enforced in constructor or entity methods
- Properties immutable (private setters) or mutated through explicit methods
- Business validation throws domain exceptions, not generic ones
- No reference to Application, Infrastructure, or any framework type
Layer 2: Application
Assembly: PredictiveAnalyticsManager.Application
Namespace prefix: Prophet.SaaS.PredictiveAnalyticsManager.Application
Depends on: Domain only.
This layer orchestrates the domain. A use case answers the question "what does the system do?" It coordinates domain objects and calls ports (interfaces), but it does not know how those ports are implemented — that is Infrastructure's job.
What belongs here
- DTOs — data shapes that cross the API boundary (
CreateFeatureDto,FeatureDto). All must extendResourceObjectand be decorated with[ApiResourceObject]. - Port interfaces (repository interfaces) — the contracts that Infrastructure must implement,
placed in
<Feature>/Ports/. They extend base port interfaces fromApplication.Base.Ports:IBaseRepository<TEntity, TFilter>— full CRUDIBaseReadRepository<TEntity, TLite, TFilter>— reads + lite projections- Add feature-specific query methods as additional interface members.
- Use case interfaces — one per verb, placed in
<Feature>/UseCases/. - Use case implementations — extend the appropriate base class (see table below), placed in
<Feature>/UseCases/Implementations/. - Filter/query models — classes that carry filtering parameters from the controller through
to the repository, placed in
<Feature>/Filters/orApplication.Base.Filters.
Base use case classes to extend
| Base class | Use when |
|---|---|
WriteUseCase<TEntity, TCreate, TResponse, TFilter, TException> | Create only (no update) |
CreateOrUpdateUseCase<TEntity, TCreate, TResponse, TUpdate, TFilter, TException> | Create and Update (PATCH/PUT) |
GetLiteUseCase<TEntity, TLite, TDto, TFilter> | Retrieving a paged/filtered list |
GetUseCase<TEntity, TDto, TFilter> | Retrieving a single entity by ID |
| Custom class | Delete or any non-standard operation |
Override ValidateBeforeCreate(TCreateDto, TEntity) when uniqueness or business rule checks
require a repository read — this is the right place for that, not in the domain entity itself or
in the controller.
DI registration
Use cases are registered in Application/Configurations/ServiceCollectionExtensions.cs
inside the AddUseCases() method using AddTransient.
What does NOT belong here
- Any reference to
Entity Framework,Dapper,Amazon.SQS,AWSSDK.*, or any concrete infrastructure technology - The
ServiceCollectionExtensionsin Application should only register use cases — not repositories or infrastructure concerns - Direct HTTP client calls
Exception handling
Let domain exceptions propagate unchanged — they carry business meaning and callers need to handle them selectively. Only catch infrastructure exceptions (DB failures, AWS timeouts), log them, and rethrow as a meaningful application-level exception:
catch (EntityConflictException) { throw; } // domain exception — propagate
catch (JobLaunchFailedException ex)
{
_logger.LogError(ex, "Job launch failed for {Id}", id);
throw new RunProcessFailedException("Process launch failed.");
}
Feature folder layout
Application/
<Feature>/
Dtos/
CreateFeatureDto.cs
FeatureDto.cs
Filters/
FeatureFilterQueryModel.cs ← optional, if custom filtering needed
Ports/
IFeatureRepository.cs
UseCases/
ICreateOrUpdateFeatureUseCase.cs
IGetFeaturesUseCase.cs
IDeleteFeatureUseCase.cs
Implementations/
CreateOrUpdateFeatureUseCase.cs
GetFeaturesUseCase.cs
DeleteFeatureUseCase.cs
Checklist
- Each use case implements a corresponding interface
- All infrastructure interactions go through port interfaces
- Business logic delegated to domain entities — not written in use cases
- Domain exceptions propagate; infrastructure exceptions are caught, logged, and rethrown
- Results mapped to DTOs before returning to the caller
Layer 3: Infrastructure
Assembly: PredictiveAnalyticsManager.Infrastructure
Namespace prefix: Prophet.SaaS.PredictiveAnalyticsManager.Infrastructure
Depends on: Application + Domain.
This layer contains everything that touches the outside world: databases, AWS services, file systems. Its job is to implement the port interfaces declared in Application — it adapts external systems to the contracts the application expects.
What belongs here
- Repository implementations — implement
IFeatureRepositoryfrom Application. - DB models — POCOs that map to database tables (
FeatureDbModel.cs). These are purely DB concerns and must not leak into the Application or Domain layers. - Data access handlers — query/command handlers for Dapper or other DB access, in
<Feature>/DataAccess/. - AWS client wrappers — S3, SQS, Batch, STS integrations (see AWS SDK skill).
- AutoMapper mappings — profiles that translate between Domain entities and DB models or DTOs,
in
Configurations/InfrastructureMappingProfile.cs. - Sort query support —
IFeatureSortQuerySupportimplementations in<Feature>/.
DI registration
Repositories and sort support are registered in Infrastructure/Configurations/ServiceCollectionExtensions.cs.
Use AddScoped for repositories.
What does NOT belong here
- Business logic — it may seem convenient to put a guard inside a repository, but rules belong in the domain.
- Direct controller dependencies — Infrastructure should never know about HTTP concepts.
Feature folder layout
Infrastructure/
<Feature>/
FeatureDbModel.cs
FeatureRepository.cs
IFeatureSortQuerySupport.cs ← optional
FeatureSortQuerySupport.cs ← optional
DataAccess/
IFeatureDbContext.cs ← interface extending IDbContext<FeatureDbModel, FeatureLite>
FeatureDbContext.cs ← delegates to FeatureDbSource
FeatureDbSource.cs ← wraps each operation in a transaction; delegates to QueryExecutor
Handler/
FeatureDbReader.cs ← maps DbDataReader columns → FeatureDbModel / FeatureLite
FeatureQueryExecutor.cs ← SQL constants + InsertAsync / UpdateAsync / GetByIdQuery / FindQuery
Data access call chain (read or write):
Repository.UpdateAsync(domainEntity)
→ MapToDbModel(entity) ← AutoMapper: domain → DB model
→ DbContext.UpdateAsync(dbModel)
→ DbSource.UpdateAsync(dbModel, isAsync)
→ ExecuteInTransaction(...)
→ QueryExecutor.UpdateAsync(dbFactory, dbModel, transaction)
→ ExecuteNonQuery(UPDATE_
---
*Content truncated.*