Skip to content

Working with How To Guides

David Kallesen edited this page Sep 10, 2026 · 3 revisions

πŸ“‹ How-To Guides

Practical recipes for common tasks when working with the ATC REST API Source Generator.


πŸ§ͺ Testing Generated Handlers

The generator creates handler interfaces (e.g., IListPetsHandler). You implement them, and the generated endpoints call your implementations. Here's how to test them.

Step 1: Implement the Handler

public class ListPetsHandler : IListPetsHandler
{
    private readonly IPetRepository repository;

    public ListPetsHandler(IPetRepository repository)
    {
        this.repository = repository;
    }

    public async Task<ListPetsResult> ExecuteAsync(
        ListPetsParameters parameters,
        CancellationToken cancellationToken)
    {
        var pets = await repository.GetAllAsync(parameters.Limit, cancellationToken);
        return ListPetsResult.Ok(pets);
    }
}

Step 2: Write the Test

public class ListPetsHandlerTests
{
    [Fact]
    public async Task ExecuteAsync_ReturnsOk_WithPets()
    {
        // Arrange
        var mockRepo = Substitute.For<IPetRepository>();
        mockRepo.GetAllAsync(10, Arg.Any<CancellationToken>())
            .Returns(new List<Pet> { new("Buddy", "Dog") });

        var handler = new ListPetsHandler(mockRepo);
        var parameters = new ListPetsParameters(Limit: 10);

        // Act
        var result = await handler.ExecuteAsync(parameters, CancellationToken.None);

        // Assert
        result.StatusCode.Should().Be(200);
    }
}

Tips

  • Test the handler directly β€” not through HTTP. The handler is a plain class with no HTTP dependencies.
  • Use NSubstitute or Moq for mocking dependencies (repositories, services).
  • The generated *Parameters records are simple to construct β€” just use the constructor.
  • The generated *Result types have static factory methods (Ok(), NotFound(), BadRequest()) for assertions.

βœ… Adding Custom Validators

The generator auto-discovers FluentValidation validators and registers them in the DI container.

Step 1: Create a Validator

Create a validator class in your project that validates a generated parameter type:

using FluentValidation;

public class CreatePetParametersValidator : AbstractValidator<CreatePetParameters>
{
    public CreatePetParametersValidator()
    {
        RuleFor(x => x.Name)
            .NotEmpty()
            .MaximumLength(100);

        RuleFor(x => x.Tag)
            .MaximumLength(50)
            .When(x => x.Tag is not null);
    }
}

Step 2: Install FluentValidation

dotnet add package FluentValidation.DependencyInjectionExtensions

Step 3: It's Automatic

The generator scans your assembly for validators and generates ServiceCollectionExtensions that registers them:

// Generated automatically
services.AddSingleton<IValidator<CreatePetParameters>, CreatePetParametersValidator>();

How Discovery Works

  • The Domain Generator (ApiServerDomainGenerator) scans your assembly at compile time
  • It finds all types implementing IValidator<T> where T matches a generated parameter type
  • Registration code is generated in ServiceCollectionExtensions.g.cs

Tips

  • Name validators with the Validator suffix by convention (e.g., CreatePetParametersValidator)
  • Validators are registered as singletons β€” don't inject scoped services into them
  • The generated endpoints automatically apply validation before calling your handler

πŸ”„ CI/CD Auto-Regeneration

Set up GitHub Actions to automatically regenerate clients when the OpenAPI spec changes.

GitHub Action Template

The project includes a ready-to-use template at docs/github-action-regenerate.yml. Copy it to your repository:

cp docs/github-action-regenerate.yml .github/workflows/regenerate-client.yml

What It Does

  1. Triggers on push to main when .yaml/.yml files change
  2. Installs the atc-rest-api-gen CLI tool
  3. Regenerates the TypeScript client
  4. Optionally runs npx tsc --noEmit to verify the output compiles
  5. Opens a PR with the regenerated files

Key Configuration

Edit the workflow file to match your project:

- name: Regenerate TypeScript client
  run: |
    atc-rest-api-gen generate client-typescript \
      -s path/to/your/api.yaml \
      -o src/api \
      --hooks ReactQuery \
      --zod

For C# Server Projects

Since the C# server uses a Roslyn Source Generator, regeneration happens automatically on dotnet build. No GitHub Action needed β€” just commit the updated YAML and build.

npm Script Alternative

For local development, add a script to package.json:

{
  "scripts": {
    "generate-api": "dotnet run --project path/to/Cli.csproj -- generate client-typescript -s api.yaml -o src/api --hooks ReactQuery"
  }
}

🧩 Extending with Partial Classes

Use generatePartialModels to extend generated records with custom logic without modifying generated files.

Step 1: Enable Partial Models

In your .atc-rest-api-client or .atc-rest-api-server marker file:

{
  "generatePartialModels": true
}

Step 2: Generated Output

The generator produces partial records:

// Auto-generated β€” do not edit
public partial record Pet(
    long Id,
    string Name,
    string? Tag);

Step 3: Extend in Your Code

Create a file in your project with the matching namespace and partial type:

namespace MyApi.Generated.Models;

public partial record Pet
{
    /// <summary>
    /// Display-friendly name with tag.
    /// </summary>
    public string DisplayName => Tag is not null ? $"{Name} ({Tag})" : Name;

    /// <summary>
    /// Validates business rules beyond what the API contract covers.
    /// </summary>
    public bool IsValid() => !string.IsNullOrWhiteSpace(Name) && Id > 0;
}

What You Can Add

Extension Example
Computed properties DisplayName, FullAddress, IsExpired
Methods Validate(), ToDto(), Clone()
Operator overloads Equality, comparison
Interface implementations IComparable<T>, custom interfaces
Nested types Companion builders, mappers

Tips

  • Keep partial extensions in a separate folder (e.g., Models/Extensions/) so they don't get confused with generated files
  • Generated files have the // <auto-generated /> header β€” your extensions don't
  • If you don't need extensions, leave generatePartialModels off (default) for simpler code
  • The x-implements extension on schemas can also add interface implementations without partial classes

Extending Generated Endpoint Results

generatePartialModels covers models. In EndpointPerOperation client mode, the generated {Operation}EndpointResult classes are partial unconditionally β€” no flag needed:

// Your file β€” same namespace as the generated result
public partial class GetPetByIdEndpointResult
{
    /// <summary>
    /// True when the pet was found and carries a tag.
    /// </summary>
    public bool IsTaggedPet => IsOk && OkContent?.Tag is not null;
}

The generated typed client class stays sealed; depend on its generated I{ClientName} interface instead of deriving from it.


πŸ“ Injecting ILogger into Handlers

By default, handler scaffolds have no constructor. Enable injectLogger to get structured logging out of the box.

Enable in Marker File

In your .atc-rest-api-server-handlers marker file:

{
    "injectLogger": true
}

Generated Output

With injectLogger: true, every handler scaffold gets:

public sealed class GetPetByIdHandler : IGetPetByIdHandler
{
    private readonly ILogger<GetPetByIdHandler> logger;

    public GetPetByIdHandler(ILogger<GetPetByIdHandler> logger)
    {
        this.logger = logger;
    }

    public Task<GetPetByIdResult> ExecuteAsync(
        GetPetByIdParameters parameters,
        CancellationToken cancellationToken = default)
    {
        // TODO: Implement getPetById logic
        throw new NotImplementedException("getPetById not implemented");
    }
}

You can then use logger directly in your implementation:

public async Task<GetPetByIdResult> ExecuteAsync(
    GetPetByIdParameters parameters,
    CancellationToken cancellationToken = default)
{
    logger.LogInformation("Getting pet {PetId}", parameters.PetId);

    var pet = await repository.GetByIdAsync(parameters.PetId, cancellationToken);
    if (pet is null)
    {
        logger.LogWarning("Pet {PetId} not found", parameters.PetId);
        return GetPetByIdResult.NotFound();
    }

    return GetPetByIdResult.Ok(pet);
}

πŸ’‘ Note: injectLogger only affects new handler files. Existing handlers are never overwritten β€” the Domain Generator skips files that already exist on disk.


πŸ”— Related Guides

🏠 Home

πŸ’Ό Why This Tool?

πŸ“– Getting Started

βš™οΈ Features

🌐 Frontend

πŸ“‹ Reference


πŸ”— Resources

Clone this wiki locally