-
Notifications
You must be signed in to change notification settings - Fork 0
PetStore API Example
davidkallesen edited this page Mar 12, 2026
·
1 revision
This sample demonstrates all generators working together in a realistic ASP.NET Core 10.0 application with OpenAPI/Scalar documentation.
- ποΈ Complete 3-layer architecture (API β Domain β DataAccess)
- π€ All generators in harmony (DependencyRegistration + OptionsBinding + Mapping)
- π OpenAPI/Swagger integration with XML documentation
- π§ Type-safe configuration with validation
- β¨ Zero boilerplate across all layers
- π Production-ready patterns for modern .NET applications
- π PetStore.Api - ASP.NET Core Minimal API with OpenAPI/Scalar
- π§ PetStore.Domain - Domain layer (services, validation, mappings)
- πΎ PetStore.DataAccess - Data access layer (repositories, entities)
- π PetStore.Api.Contract - API contracts (DTOs, requests/responses)
graph TB
subgraph "Client"
HTTP[HTTP Client]
SCALAR[Scalar UI]
end
subgraph "PetStore.Api (ASP.NET Core 10.0)"
EP[Endpoints]
OPENAPI[OpenAPI Generator]
PROG[Program.cs]
APIENUM["PetStatus (API)"]
end
subgraph "PetStore.Domain"
PS["PetService - @Registration"]
VS["ValidationService - @Registration"]
BG["PetMaintenanceService - @Registration (BackgroundService)"]
OPT["PetStoreOptions - @OptionsBinding"]
OPT2["PetMaintenanceServiceOptions - @OptionsBinding"]
PET["Pet - @MapTo(PetResponse)"]
PET2["Pet - @MapTo(PetEntity, Bidirectional=true)"]
DOMENUM["PetStatus (Domain)"]
end
subgraph "PetStore.DataAccess"
PR["PetRepository - @Registration"]
ENT["PetEntity"]
ENTENUM["PetStatusEntity"]
end
subgraph "PetStore.Api.Contract"
DTO[DTOs: PetResponse, CreatePetRequest]
end
subgraph "Generated Code"
DI1["AddDependencyRegistrationsFromPetStoreApi()"]
DI2["AddDependencyRegistrationsFromPetStoreDomain()"]
DI3["AddDependencyRegistrationsFromPetStoreDataAccess()"]
CFG["AddOptionsFromPetStoreDomain(config)"]
M1["Pet.MapToPetResponse()"]
M2["Pet.MapToPetEntity() [Bidirectional]"]
M3["PetEntity.MapToPet() [Bidirectional]"]
end
HTTP -->|POST /pets| EP
HTTP -->|GET /pets/:id| EP
SCALAR -->|Browse API| OPENAPI
EP --> PS
PS --> VS
PS --> PR
PS --> OPT
PR --> ENT
ENT --> ENTENUM
PET --> DOMENUM
EP --> APIENUM
PROG --> DI2
PROG --> CFG
PET --> M1
PET2 -.->|Bidirectional| M2
PET2 -.->|Bidirectional| M3
style PS fill:#0969da
style VS fill:#0969da
style BG fill:#0969da
style PR fill:#0969da
style OPT fill:#0969da
style OPT2 fill:#0969da
style DI1 fill:#2ea44f
style DI2 fill:#2ea44f
style DI3 fill:#2ea44f
style CFG fill:#2ea44f
style M1 fill:#2ea44f
style M2 fill:#2ea44f
style M3 fill:#2ea44f
style ENTENUM fill:#d73a4a
style DOMENUM fill:#d73a4a
style APIENUM fill:#d73a4a
PetStore.Api
βββ PetStore.Domain
β βββ PetStore.DataAccess (NO Api.Contract reference)
β βββ PetStore.Api.Contract
βββ PetStore.Api.Contract
Key: DataAccess has NO upward dependencies, maintaining clean architecture
sequenceDiagram
participant Client
participant API as API Endpoint
participant PetService as PetService [Registration]
participant Options as PetStoreOptions [OptionsBinding]
participant Repository as PetRepository [Registration]
participant Mapping as Generated Mappings
Note over Client,Mapping: POST /pets
Client->>API: HTTP POST /pets
Note over API: CreatePetRequest DTO
API->>PetService: CreatePet(request)
PetService->>Options: Get MaxPetsPerPage
Options-->>PetService: 100
Note over PetService: Create Pet with Status = Models.PetStatus.Available
PetService->>PetService: new Pet { Status = PetStatus.Available }
Note right of Mapping: Bidirectional Generated Code
PetService->>Mapping: pet.MapToPetEntity()
Note right of Mapping: Enum cast: PetStatus β PetStatusEntity
Mapping-->>PetService: PetEntity
PetService->>Repository: Create(entity)
Repository->>Repository: Save to storage
Note right of Mapping: Bidirectional Generated Code
Repository->>Mapping: entity.MapToPet()
Note right of Mapping: Enum cast: PetStatusEntity β PetStatus
Mapping-->>Repository: Pet
Repository-->>PetService: Pet
Note right of Mapping: Generated Code
PetService->>Mapping: pet.MapToPetResponse()
Mapping-->>PetService: PetResponse
PetService-->>API: PetResponse
API-->>Client: 200 OK + PetResponse
sequenceDiagram
participant Client
participant API as API Endpoint
participant PetService as PetService [Registration]
participant Repository as PetRepository [Registration]
participant Mapping as Generated Mappings
Note over Client,Mapping: GET /pets/{id}
Client->>API: HTTP GET /pets/{id}
API->>PetService: GetById(id)
PetService->>Repository: GetById(id)
Note right of Mapping: Bidirectional Generated Code
Repository->>Repository: Retrieve PetEntity from storage
Repository->>Mapping: entity.MapToPet()
Note right of Mapping: Enum cast: PetStatusEntity β PetStatus
Mapping-->>Repository: Pet
Repository-->>PetService: Pet
Note right of Mapping: Generated Code
PetService->>Mapping: pet.MapToPetResponse()
Mapping-->>PetService: PetResponse
PetService-->>API: PetResponse
API-->>Client: 200 OK + PetResponse
{
"PetStore": {
"MaxPetsPerPage": 100
},
"Logging": {
"LogLevel": {
"Default": "Information"
}
}
}namespace PetStore.Api.Contract;
public class CreatePetRequest
{
public string Name { get; set; } = string.Empty;
public string Species { get; set; } = string.Empty;
public string Breed { get; set; } = string.Empty;
public int Age { get; set; }
}
public class PetResponse
{
public Guid Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Species { get; set; } = string.Empty;
public string Breed { get; set; } = string.Empty;
public int Age { get; set; }
public PetStatus Status { get; set; }
public DateTimeOffset CreatedAt { get; set; }
}
public enum PetStatus
{
Available = 0,
Pending = 1,
Adopted = 2,
}using Atc.SourceGenerators.Annotations;
using System.ComponentModel.DataAnnotations;
namespace PetStore.Domain;
// β¨ Options bound automatically
[OptionsBinding("PetStore", ValidateDataAnnotations = true, ValidateOnStart = true)]
public partial class PetStoreOptions
{
[Range(1, 100)]
public int MaxPetsPerPage { get; set; } = 20;
[Required]
[StringLength(100, MinimumLength = 1)]
public string StoreName { get; set; } = "Furry Friends Pet Store";
public bool EnableAutoStatusUpdates { get; set; } = true;
}
// β¨ Auto-registered as IPetService (default: Singleton)
[Registration]
public class PetService : IPetService
{
private readonly IPetRepository repository;
private readonly PetStoreOptions options;
public PetService(
IPetRepository repository,
IOptions<PetStoreOptions> options)
{
ArgumentNullException.ThrowIfNull(options);
this.repository = repository;
this.options = options.Value;
}
public Pet? GetById(Guid id)
{
var entity = repository.GetById(id);
return entity?.MapToPet(); // β¨ Bidirectional mapping (PetEntity β Pet)
}
public IEnumerable<Pet> GetAll() =>
repository
.GetAll()
.Select(e => e.MapToPet()) // β¨ Bidirectional mapping
.Take(options.MaxPetsPerPage);
public Pet CreatePet(CreatePetRequest request)
{
ArgumentNullException.ThrowIfNull(request);
var pet = new Pet
{
Id = Guid.NewGuid(),
Name = request.Name,
Species = request.Species,
Breed = request.Breed,
Age = request.Age,
Status = Models.PetStatus.Available,
CreatedAt = DateTimeOffset.UtcNow,
};
var entity = pet.MapToPetEntity(); // β¨ Bidirectional mapping (Pet β PetEntity)
var createdEntity = repository.Create(entity);
return createdEntity.MapToPet(); // β¨ Bidirectional mapping (PetEntity β Pet)
}
}
// β¨ Domain model with bidirectional mapping
[MapTo(typeof(PetResponse))]
[MapTo(typeof(PetEntity), Bidirectional = true)]
public partial class Pet
{
public Guid Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Species { get; set; } = string.Empty;
public string Breed { get; set; } = string.Empty;
public int Age { get; set; }
public PetStatus Status { get; set; }
public DateTimeOffset CreatedAt { get; set; }
}
public enum PetStatus
{
Available = 0,
Pending = 1,
Adopted = 2,
}using Atc.SourceGenerators.Annotations;
namespace PetStore.DataAccess;
// β¨ Auto-registered as IPetRepository (Singleton lifetime)
[Registration(Lifetime.Singleton)]
public class PetRepository : IPetRepository
{
private readonly Dictionary<Guid, PetEntity> pets;
public PetRepository()
{
var pet1Id = Guid.Parse("11111111-1111-1111-1111-111111111111");
var pet2Id = Guid.Parse("22222222-2222-2222-2222-222222222222");
pets = new Dictionary<Guid, PetEntity>
{
[pet1Id] = new PetEntity
{
Id = pet1Id, Name = "Buddy", Species = "Dog",
Breed = "Golden Retriever", Age = 3,
Status = Entities.PetStatusEntity.Available,
CreatedAt = DateTimeOffset.UtcNow.AddDays(-30),
},
[pet2Id] = new PetEntity
{
Id = pet2Id, Name = "Whiskers", Species = "Cat",
Breed = "Siamese", Age = 2,
Status = Entities.PetStatusEntity.Adopted,
CreatedAt = DateTimeOffset.UtcNow.AddDays(-45),
},
};
}
public PetEntity? GetById(Guid id) =>
!pets.TryGetValue(id, out var entity) ? null : entity;
public IEnumerable<PetEntity> GetAll() => pets.Values;
public PetEntity Create(PetEntity pet) { pets[pet.Id] = pet; return pet; }
}
// β¨ Entity - no MapTo attribute needed (Pet has Bidirectional = true)
public class PetEntity
{
public Guid Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Species { get; set; } = string.Empty;
public string Breed { get; set; } = string.Empty;
public int Age { get; set; }
public PetStatusEntity Status { get; set; }
public DateTimeOffset CreatedAt { get; set; }
}
public enum PetStatusEntity
{
Available = 0,
Pending = 1,
Adopted = 2,
}using Microsoft.AspNetCore.Mvc;
var builder = WebApplication.CreateBuilder(args);
// β¨ Register all services transitively (Domain + DataAccess)
builder.Services.AddDependencyRegistrationsFromPetStoreDomain(
includeReferencedAssemblies: true);
// β¨ Register configuration options automatically
builder.Services.AddOptionsFromPetStoreDomain(
builder.Configuration,
includeReferencedAssemblies: true);
builder.Services.AddOpenApi();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
app.MapScalarApiReference();
}
app.UseHttpsRedirection();
// β¨ API Endpoints
app.MapPost("/pets", ([FromBody] CreatePetRequest request, IPetService petService) =>
{
var pet = petService.CreatePet(request);
var response = pet.MapToPetResponse(); // β¨ Generated mapping
return Results.Created($"/pets/{response.Id}", response);
})
.WithName("CreatePet")
.Produces<PetResponse>(StatusCodes.Status201Created)
.WithOpenApi();
app.MapGet("/pets/{id}", ([FromRoute] Guid id, IPetService petService) =>
{
var pet = petService.GetById(id);
if (pet is null) return Results.NotFound();
return Results.Ok(pet.MapToPetResponse()); // β¨ Generated mapping
})
.WithName("GetPetById")
.Produces<PetResponse>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound);
app.MapGet("/pets", (IPetService petService) =>
{
var pets = petService.GetAll();
var response = pets.Select(p => p.MapToPetResponse());
return Results.Ok(response);
})
.WithName("GetAllPets")
.Produces<IEnumerable<PetResponse>>(StatusCodes.Status200OK);
app.Run();// From PetStore.Domain (with includeReferencedAssemblies: true)
services.AddSingleton<IPetService, PetService>();
services.AddHostedService<PetMaintenanceService>(); // β¨ Automatic BackgroundService registration
services.AddSingleton<IPetRepository, PetRepository>(); // From referenced PetStore.DataAccess// From PetStore.Domain
services.AddOptions<PetStoreOptions>()
.Bind(configuration.GetSection("PetStore"))
.ValidateDataAnnotations()
.ValidateOnStart();// Pet β PetResponse (one-way)
public static PetResponse MapToPetResponse(this Pet source) { ... }
// Pet β PetEntity (bidirectional)
public static PetEntity MapToPetEntity(this Pet source) { ... }
// PetEntity β Pet (bidirectional reverse - generated automatically!)
public static Pet MapToPet(this PetEntity source) { ... }Each layer has its own enum to maintain proper separation of concerns:
- π API Layer:
PetStatus(Api.Contract) - exposed to clients - π§ Domain Layer:
PetStatus(Domain.Models) - business logic - πΎ DataAccess Layer:
PetStatusEntity- database persistence
Single attribute generates both forward and reverse mappings:
[MapTo(typeof(PetEntity), Bidirectional = true)]
public partial class Pet { ... }
// Generates:
// - Pet.MapToPetEntity() (forward)
// - PetEntity.MapToPet() (reverse - automatically!)Single registration call includes all referenced assemblies:
builder.Services.AddDependencyRegistrationsFromPetStoreDomain(
includeReferencedAssemblies: true); // Also registers from PetStore.DataAccess| Generator | Without | With | Savings |
|---|---|---|---|
| β‘ DependencyRegistration | ~20 lines | 1 line (transitive) | 95% less code |
| βοΈ OptionsBinding | ~5 lines per options class | 1 line (transitive) | 80% less code |
| πΊοΈ Mapping | ~30 lines (forward + reverse) | 1 attribute (bidirectional) | 97% less code |
Total: From ~200 lines of boilerplate to ~2 lines π
cd sample/PetStore.Api
dotnet runThen open your browser to:
- π Scalar UI: https://localhost:42616/scalar/v1
- π OpenAPI JSON: https://localhost:42616/openapi/v1.json
β Create a pet:
curl -X POST https://localhost:42616/pets \
-H "Content-Type: application/json" \
-d '{"name":"Charlie","species":"Dog","breed":"Labrador","age":4}'π Get all pets:
curl https://localhost:42616/petsπ Get a specific pet:
curl https://localhost:42616/pets/11111111-1111-1111-1111-111111111111π·οΈ Get pets by status:
curl https://localhost:42616/pets/status/Availableπ See Also:
- π Getting Started β Installation and setup
- β‘ Working with Dependency Registration β DI service registration
- βοΈ Working with Options Binding β Configuration binding
- πΊοΈ Working with Object Mapping β Object-to-object mapping
- π Working with Enum Mapping β Enum-to-enum mapping
- π¦ Sample Projects β All sample project overviews
π Home
- π Getting Started
- π Migration Guide
- π Working with Dependency Registration
- βοΈ Working with Options Binding
- πΊοΈ Working with Object Mapping
- π Working with Enum Mapping
- π Working with Annotation Constants
- π¦ Sample Projects
- πΎ PetStore API Example
- π Attribute API Reference
- π©Ί Diagnostics Reference
β οΈ Common Patterns and Anti-Patterns- π Native AOT Compatibility
- π§ Troubleshooting
- π Dependency Registration Feature Roadmap
- βοΈ Options Binding Feature Roadmap
- πΊοΈ Object Mapping Feature Roadmap
- π Annotation Constants Feature Roadmap
- π¦ GitHub Repository
- π₯ NuGet Package
- π Report Issues