Skip to content

PetStore API Example

davidkallesen edited this page Mar 12, 2026 · 1 revision

🐾 PetStore API Example

This sample demonstrates all generators working together in a realistic ASP.NET Core 10.0 application with OpenAPI/Scalar documentation.

🎯 Focus

  • πŸ—οΈ 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

πŸ“ Projects

  • 🌐 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)

πŸ—οΈ Architecture

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
Loading

🧱 Project References (Clean Architecture)

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

πŸ”„ Request Flow: Creating a Pet

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
Loading

πŸ”„ Request Flow: Getting a Pet

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
Loading

πŸ’» Code Example

βš™οΈ Configuration (appsettings.json)

{
  "PetStore": {
    "MaxPetsPerPage": 100
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information"
    }
  }
}

πŸ“‹ PetStore.Api.Contract (DTOs)

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,
}

🧠 PetStore.Domain (Services & Models)

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,
}

πŸ’Ύ PetStore.DataAccess (Repository & Entities)

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,
}

🌐 PetStore.Api (Application Setup)

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();

πŸ“ Generated Code Summary

⚑ DependencyRegistration Generator

// From PetStore.Domain (with includeReferencedAssemblies: true)
services.AddSingleton<IPetService, PetService>();
services.AddHostedService<PetMaintenanceService>();  // ✨ Automatic BackgroundService registration
services.AddSingleton<IPetRepository, PetRepository>();  // From referenced PetStore.DataAccess

βš™οΈ OptionsBinding Generator

// From PetStore.Domain
services.AddOptions<PetStoreOptions>()
    .Bind(configuration.GetSection("PetStore"))
    .ValidateDataAnnotations()
    .ValidateOnStart();

πŸ—ΊοΈ Mapping Generator

// 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) { ... }

✨ Key Features Demonstrated

🎨 Clean Architecture with Enum Separation

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

πŸ”„ Bidirectional Mapping

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!)

πŸ”— Transitive Registration

Single registration call includes all referenced assemblies:

builder.Services.AddDependencyRegistrationsFromPetStoreDomain(
    includeReferencedAssemblies: true);  // Also registers from PetStore.DataAccess

πŸ“Š Zero Boilerplate

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 πŸŽ‰

πŸš€ Running the Sample

cd sample/PetStore.Api
dotnet run

Then open your browser to:

πŸ§ͺ Try It Out

βž• 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:

🏠 Home

πŸ“– Getting Started

⚑ Generators

🎯 Examples

πŸ”— Integrations

πŸ” Reference

πŸ“‹ Feature Roadmaps


πŸ”— Resources

Clone this wiki locally