agentskills.codes
ON

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.zip

Installs 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).
628 chars✓ has a “when” triggerlonger than Claude Code's old 250-char listing cap (fine on current versions)

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 referencing BaseLiteModel (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

ClassUse when
BaseModelFull entity with Id, CreatedBy, CreatedAt, ModifiedBy, ModifiedAt
BaseLiteModelLightweight 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 BaseModel or BaseLiteModel
  • 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 extend ResourceObject and be decorated with [ApiResourceObject].
  • Port interfaces (repository interfaces) — the contracts that Infrastructure must implement, placed in <Feature>/Ports/. They extend base port interfaces from Application.Base.Ports:
    • IBaseRepository<TEntity, TFilter> — full CRUD
    • IBaseReadRepository<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/ or Application.Base.Filters.

Base use case classes to extend

Base classUse 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 classDelete 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 ServiceCollectionExtensions in 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 IFeatureRepository from 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 supportIFeatureSortQuerySupport implementations 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.*

Search skills

Search the agent skills registry