Adapter Develop
project-spec -> architecture-design -> domain-develop -> application-develop -> adapter-develop -> observability-develop -> test-develop
Prerequisites
Section titled “Prerequisites”- Reads
application/03-implementation-results.mdgenerated by theapplication-developskill to confirm the Port list (IRepository, IQueryPort, External Service). - If
01-architecture-design.mdgenerated by thearchitecture-designskill exists, it is read to confirm folder structure and persistence strategy. - If prerequisite documents are missing, the user is asked directly.
Background
Section titled “Background”Once Port interfaces in the Application layer are defined, Adapters that implement them must be written. From InMemory Repository, EF Core Repository, Dapper Query Adapter, FastEndpoints endpoint, to DI registration — each Adapter type has repeating structures (base class inheritance, [GenerateObservablePort], IO.lift/IO.liftAsync wrapping, Mapper, PropertyMap).
The /adapter-develop skill automates this repetition. When you provide Port interfaces and requirements, it generates Adapter implementations, Mappers, and DI registration code matching Functorium framework patterns in 4 steps.
Skill Overview
Section titled “Skill Overview”4-Step Process
Section titled “4-Step Process”| Step | Task | Deliverable |
|---|---|---|
| 1 | Port -> Adapter mapping | Determine Adapter type per Port interface |
| 2 | Implementation generation | Adapter classes, Model, Mapper, Configuration |
| 3 | DI registration | RegisterScopedObservablePort registration code |
| 4 | EF Core setup | DbContext, Migration, Query Filter (if applicable) |
Supported Adapters
Section titled “Supported Adapters”| Adapter Type | Base Class | IO Wrapping | Description |
|---|---|---|---|
| InMemory Repository | InMemoryRepositoryBase<T, TId> | IO.lift | In-memory store for testing |
| EF Core Repository | EfCoreRepositoryBase<T, TId, TModel> | IO.liftAsync | Persistent store |
| Dapper Query | DapperQueryBase<T, TDto> | — | CQRS Read Side |
| FastEndpoints | Endpoint<TReq, TRes> | — | HTTP endpoint |
| External API | Direct implementation | IO.liftAsync | External HTTP API integration |
Core API Patterns
Section titled “Core API Patterns”| Pattern | Usage |
|---|---|
| Observable Port | Apply [GenerateObservablePort] attribute |
| Synchronous wrapping | IO.lift(() => Fin.Succ(value)) |
| Asynchronous wrapping | IO.liftAsync(async () => { ... return Fin.Succ(result); }) |
| Adapter error | AdapterError.For<TAdapter>(new NotFound(), id, message) |
| DI registration | services.RegisterScopedObservablePort<IPort, AdapterObservable>() |
| Mapper | internal static class extension methods, ToModel()/ToDomain() |
Naming Conventions + Folder Structure
Section titled “Naming Conventions + Folder Structure”Adapter projects follow the 3-dimensional folder structure.
{Name}.Adapters.Persistence/└── {Aggregate}/ # Primary: Aggregate (what) ├── {Aggregate}.Model.cs # DB POCO ├── {Aggregate}.Configuration.cs # EF Core Fluent API ├── {Aggregate}.Mapper.cs # Domain <-> Model conversion ├── Repositories/ # Secondary: CQRS Role (write) │ ├── {Aggregate}RepositoryEfCore.cs # Tertiary: Technology (how) │ └── {Aggregate}RepositoryInMemory.cs └── Queries/ # Secondary: CQRS Role (read) ├── {Aggregate}QueryDapper.cs └── {Aggregate}QueryInMemory.csNaming Pattern: {Subject}{Role}{Variant} (e.g., ProductRepositoryEfCore, ProductQueryDapper)
ObservableSignal — Adapter Internal Operational Logging
Section titled “ObservableSignal — Adapter Internal Operational Logging”Separately from the automatic observability of Observable Port, ObservableSignal is used when developers need to directly write operational logs inside an Adapter.
public override FinT<IO, Product> GetById(ProductId id){ return IO.liftAsync(async () => { var model = await ReadQuery().FirstOrDefaultAsync(ByIdPredicate(id)); if (model is null) { ObservableSignal.Info("cache_miss", new { ProductId = id.ToString() }); return NotFoundError(id); } ObservableSignal.Info("cache_hit", new { ProductId = id.ToString() }); return Fin.Succ(ToDomain(model)); });}ObservableSignal supplementary fields use the adapter.* prefix. These fields are propagated only to the Logging Pillar.
Basic Invocation
Section titled “Basic Invocation”/adapter-develop Create a product InMemory Repository.Interactive Mode
Section titled “Interactive Mode”Invoking /adapter-develop without arguments starts the skill in interactive mode, collecting requirements through conversation.
Execution Flow
Section titled “Execution Flow”- Adapter analysis — Shows the Adapter type and implementation plan per Port in a table
- User confirmation — Proceed to code generation after confirming the analysis results
- Code generation — Generates Adapter, Model, Mapper, DI registration code
- Build verification — Runs
dotnet buildto confirm passing
Example 1: Beginner — InMemory Repository
Section titled “Example 1: Beginner — InMemory Repository”The most basic Adapter pattern. Implements a test in-memory store with InMemoryRepositoryBase inheritance, [GenerateObservablePort] application, and IO.lift wrapping.
Prompt
Section titled “Prompt”/adapter-develop Create a product InMemory Repository.Expected Results
Section titled “Expected Results”| Deliverable | Type | Description |
|---|---|---|
| Repository | InMemoryProductRepository | Inherits InMemoryRepositoryBase<Product, ProductId> |
| DI Registration | RegisterScopedObservablePort | Registered as Observable version |
Key Snippets
Section titled “Key Snippets”InMemory Repository — InMemoryRepositoryBase inheritance, [GenerateObservablePort] applied:
[GenerateObservablePort]public class InMemoryProductRepository : InMemoryRepositoryBase<Product, ProductId>, IProductRepository{ internal static readonly ConcurrentDictionary<ProductId, Product> Products = new(); protected override ConcurrentDictionary<ProductId, Product> Store => Products;
public InMemoryProductRepository(IDomainEventCollector eventCollector) : base(eventCollector) { }
public virtual FinT<IO, bool> Exists(Specification<Product> spec) { return IO.lift(() => { bool exists = Products.Values.Any(p => spec.IsSatisfiedBy(p)); return Fin.Succ(exists); }); }}DI Registration — Uses the Observable version generated by Source Generator:
services.RegisterScopedObservablePort<IProductRepository, InMemoryProductRepositoryObservable>();Example 2: Intermediate — EF Core Repository + Configuration + Mapper
Section titled “Example 2: Intermediate — EF Core Repository + Configuration + Mapper”Adds a persistence layer to Example 1. Shows the EF Core Repository’s 3-argument constructor pattern (EventCollector, ApplyIncludes, PropertyMap), Persistence Model (POCO), Mapper extension methods, and EF Core Configuration.
Prompt
Section titled “Prompt”/adapter-develop Implement a product EF Core Repository. Include Soft Delete, using SQLite.Expected Results
Section titled “Expected Results”| Deliverable | Type | Description |
|---|---|---|
| Repository | EfCoreProductRepository | EfCoreRepositoryBase inheritance, Soft Delete override |
| Model | ProductModel | POCO, primitive types only |
| Configuration | ProductConfiguration | EF Core Fluent API, Query Filter |
| Mapper | ProductMapper | internal static class, ToModel()/ToDomain() |
| DI Registration | RegisterScopedObservablePort | Registered as Observable version |
Key Snippets
Section titled “Key Snippets”EF Core Repository — 3-argument constructor, PropertyMap, Soft Delete override:
[GenerateObservablePort]public class EfCoreProductRepository : EfCoreRepositoryBase<Product, ProductId, ProductModel>, IProductRepository{ private readonly LayeredArchDbContext _dbContext;
public EfCoreProductRepository(LayeredArchDbContext dbContext, IDomainEventCollector eventCollector) : base(eventCollector, q => q.Include(p => p.ProductTags), new PropertyMap<Product, ProductModel>() .Map(p => (decimal)p.Price, m => m.Price) .Map(p => (string)p.Name, m => m.Name) .Map(p => p.Id.ToString(), m => m.Id)) => _dbContext = dbContext;
protected override DbContext DbContext => _dbContext; protected override DbSet<ProductModel> DbSet => _dbContext.Products; protected override Product ToDomain(ProductModel model) => model.ToDomain(); protected override ProductModel ToModel(Product p) => p.ToModel();
// Soft Delete override public override FinT<IO, int> Delete(ProductId id) { return IO.liftAsync(async () => { var model = await ReadQueryIgnoringFilters() .FirstOrDefaultAsync(ByIdPredicate(id)); if (model is null) return NotFoundError(id);
var product = ToDomain(model); product.Delete("system"); var updatedModel = ToModel(product); DbSet.Attach(updatedModel); _dbContext.Entry(updatedModel).Property(p => p.DeletedAt).IsModified = true; _dbContext.Entry(updatedModel).Property(p => p.DeletedBy).IsModified = true; EventCollector.Track(product); return Fin.Succ(1); }); }}Mapper — internal static class, restoring domain without validation via CreateFromValidated:
internal static class ProductMapper{ public static ProductModel ToModel(this Product product) => new() { Id = product.Id.ToString(), Name = product.Name, Price = product.Price, CreatedAt = product.CreatedAt, DeletedAt = product.DeletedAt.ToNullable(), DeletedBy = product.DeletedBy.Match(Some: v => (string?)v, None: () => null), };
public static Product ToDomain(this ProductModel model) => Product.CreateFromValidated( ProductId.Create(model.Id), ProductName.CreateFromValidated(model.Name), Money.CreateFromValidated(model.Price), model.CreatedAt, Optional(model.DeletedAt), Optional(model.DeletedBy));}Example 3: Advanced — Dapper Query + Specification + Endpoint
Section titled “Example 3: Advanced — Dapper Query + Specification + Endpoint”Adds a CQRS Read Side to Example 2. Shows a Dapper-based Query Adapter, DapperSpecTranslator for converting Specifications to SQL WHERE clauses, FastEndpoints endpoint, and DI registration.
Prompt
Section titled “Prompt”/adapter-develop Implement a product search API. Dapper Query + Specification-based search + pagination + FastEndpoints.Expected Results
Section titled “Expected Results”| Deliverable | Type | Description |
|---|---|---|
| Query Adapter | DapperProductQuery | Inherits DapperQueryBase<Product, ProductSummaryDto> |
| Spec Translator | ProductSpecTranslator | Specification -> SQL WHERE clause conversion |
| Endpoint | SearchProductsEndpoint | FastEndpoints, pagination/sort support |
| DI Registration | RegisterScopedObservablePort | Query Adapter + IDbConnection registration |
Key Snippets
Section titled “Key Snippets”Dapper Query Adapter — DapperQueryBase inheritance, direct SQL writing:
[GenerateObservablePort]public class DapperProductQuery : DapperQueryBase<Product, ProductSummaryDto>, IProductQuery{ public string RequestCategory => "QueryAdapter";
protected override string SelectSql => "SELECT Id AS ProductId, Name, Price FROM Products"; protected override string CountSql => "SELECT COUNT(*) FROM Products"; protected override string DefaultOrderBy => "Name ASC"; protected override Dictionary<string, string> AllowedSortColumns { get; } = new(StringComparer.OrdinalIgnoreCase) { ["Name"] = "Name", ["Price"] = "Price" };
public DapperProductQuery(IDbConnection connection) : base(connection, ProductSpecTranslator.Instance) { }}DapperSpecTranslator — Converts Specification to SQL WHERE clause:
internal static class ProductSpecTranslator{ internal static readonly DapperSpecTranslator<Product> Instance = new DapperSpecTranslator<Product>() .WhenAll(alias => { var p = DapperSpecTranslator<Product>.Prefix(alias); return ($"WHERE {p}DeletedAt IS NULL", new DynamicParameters()); }) .When<ProductPriceRangeSpec>((spec, alias) => { var p = DapperSpecTranslator<Product>.Prefix(alias); return ($"WHERE {p}DeletedAt IS NULL AND {p}Price >= @MinPrice AND {p}Price <= @MaxPrice", DapperSpecTranslator<Product>.Params( ("MinPrice", (decimal)spec.MinPrice), ("MaxPrice", (decimal)spec.MaxPrice))); });}FastEndpoints Endpoint — HTTP response conversion with SendFinResponseAsync:
public sealed class SearchProductsEndpoint : Endpoint<SearchProductsEndpoint.Request, SearchProductsEndpoint.Response>{ private readonly IMediator _mediator;
public SearchProductsEndpoint(IMediator mediator) => _mediator = mediator;
public override void Configure() { Get("api/products/search"); AllowAnonymous(); }
public override async Task HandleAsync(Request req, CancellationToken ct) { var usecaseRequest = new SearchProductsQuery.Request( req.Name ?? "", req.MinPrice ?? 0, req.MaxPrice ?? 0, req.Page ?? 1, req.PageSize ?? PageRequest.DefaultPageSize, req.SortBy ?? "", req.SortDirection ?? "");
var result = await _mediator.Send(usecaseRequest, ct); var mapped = result.Map(r => new Response(r.Products.ToList(), r.TotalCount, r.Page, r.PageSize)); await this.SendFinResponseAsync(mapped, ct); }
public sealed record Request( [property: QueryParam] string? Name = null, [property: QueryParam] decimal? MinPrice = null, [property: QueryParam] decimal? MaxPrice = null, [property: QueryParam] int? Page = null, [property: QueryParam] int? PageSize = null, [property: QueryParam] string? SortBy = null, [property: QueryParam] string? SortDirection = null);
public new sealed record Response( List<ProductSummaryDto> Products, int TotalCount, int Page, int PageSize);}DI Registration — Per-Provider branching, using Observable versions:
// InMemoryservices.RegisterScopedObservablePort<IProductRepository, InMemoryProductRepositoryObservable>();services.RegisterScopedObservablePort<IProductQuery, InMemoryProductQueryObservable>();
// SQLite (EF Core + Dapper)services.RegisterScopedObservablePort<IProductRepository, EfCoreProductRepositoryObservable>();services.AddScoped<IDbConnection>(_ =>{ var conn = new SqliteConnection(connectionString); conn.Open(); return conn;});services.RegisterScopedObservablePort<IProductQuery, DapperProductQueryObservable>();References
Section titled “References”Workflow
Section titled “Workflow”- Workflow — 7-step overall flow
- Application Develop Skill — Previous step: Use case implementation
- Test Develop Skill — Next step: Test writing
Framework Guides
Section titled “Framework Guides”- Port Definition
- Adapter Implementation
- Pipeline and DI
- Adapter Testing
- Repository & Query Adapter Implementation Guide
- Error System: Adapter & Testing
Related Skills
Section titled “Related Skills”- Domain Develop Skill — Generate domain building blocks: Aggregate, Value Object, Event, etc.
- Application Layer Develop Skill — Generate Command/Query/EventHandler use cases
- Test Develop Skill — Generate unit/integration/architecture tests