AutoRepo 0.4.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package AutoRepo --version 0.4.0
                    
NuGet\Install-Package AutoRepo -Version 0.4.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="AutoRepo" Version="0.4.0">
  <PrivateAssets>all</PrivateAssets>
  <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="AutoRepo" Version="0.4.0" />
                    
Directory.Packages.props
<PackageReference Include="AutoRepo">
  <PrivateAssets>all</PrivateAssets>
  <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add AutoRepo --version 0.4.0
                    
#r "nuget: AutoRepo, 0.4.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package AutoRepo@0.4.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=AutoRepo&version=0.4.0
                    
Install as a Cake Addin
#tool nuget:?package=AutoRepo&version=0.4.0
                    
Install as a Cake Tool

AutoRepo

A C# source generator that automatically creates API client code from repository interfaces.

What it generates

Given a repository interface annotated with [GenerateApi], AutoRepo generates:

  1. IApi{Name} - Refit-compatible interface with HTTP method attributes
  2. Api{Name}Client - HttpClient-based implementation
  3. Fallback{Name} - Local-first wrapper with API fallback
  4. Combined{Name} - Merged wrapper that queries both local and remote sources in parallel

Usage

Add AutoRepo as an analyzer reference:

<ProjectReference Include="path/to/AutoRepo.csproj"
                  OutputItemType="Analyzer"
                  ReferenceOutputAssembly="false" />

Annotate your repository interfaces:

using AutoRepo;

[GenerateApi(RoutePrefix = "/api/products")]
public interface IProductRepository
{
    Task<IReadOnlyList<Product>> GetAllAsync(CancellationToken ct = default);
    Task<Product?> GetAsync(int id, CancellationToken ct = default);
    Task UpsertAsync(Product product, CancellationToken ct = default);
    Task DeleteAsync(int id, CancellationToken ct = default);
}

Custom JSON serialization

If your entities need custom JSON converters (e.g., for strongly-typed IDs), specify a JsonOptionsType:

[GenerateApi(RoutePrefix = "/api/products", JsonOptionsType = "MyApp.MyJsonOptions")]
public interface IProductRepository { ... }

The type must have a static Default property returning JsonSerializerOptions.

Conventions

  • HTTP method inference: Get*/Search*/Find* → GET, Delete*/Remove* → DELETE, Create*/Add* → POST, Update* → PUT, Upsert* → POST (with body) or PUT
  • Route parameters: Parameters named id or ending with Id in Get/Delete methods

Overriding the route

The route heuristic above only has an id-shaped parameter to key a path segment on. It breaks down for a family of sibling read methods distinguished by intent rather than by id — e.g. GetTrendingAsync(), GetPopularAsync(), GetTopRatedAsync() all have no route parameter at all, so without an override they'd silently collapse onto the same bare route prefix. Use [ApiRoute("...")] on the method to say exactly what the path should be instead of guessing:

[GenerateApi(RoutePrefix = "/api/discover")]
public interface IDiscoveryRepository
{
    [ApiRoute("tvshows/trending")]
    Task<DiscoveryResult> GetTrendingTVShowsAsync(int? page = null, CancellationToken ct = default);

    [ApiRoute("tvshows/popular")]
    Task<DiscoveryResult> GetPopularTVShowsAsync(int? page = null, CancellationToken ct = default);

    // "{tmdbId}" binds the parameter into the path, overriding the default id-suffix heuristic too
    [ApiRoute("tvshows/{tmdbId}/details")]
    Task<ShowDetails> GetDiscoverTVShowDetailsAsync(int tmdbId, CancellationToken ct = default);
}

[ApiRoute] always wins outright for that method — the name-based heuristic (including the automatic /search suffix for Search* methods) is skipped entirely once it's present. [ApiRoute("")] means "the bare route prefix, on purpose" — useful when one method in the interface genuinely belongs at the resource root while its siblings each need their own explicit path (e.g. a plain GetAllAsync() alongside GetContinueWatchingAsync()/GetRecentAsync(), all on the same [GenerateApi] interface).

  • Body parameters: Complex types in POST/PUT methods
  • Query parameters: Primitive types that aren't route parameters
  • Source tracking: If entities have a Source property of type DataSource, generated wrappers set it to Local, Remote, or Both
  • Fallback strategy: Single items → local-first; Collections → API-first; Mutations → local-only unless [MutationOperation(Target = ...)] says otherwise (see below)
  • Combined strategy: All reads query both sources in parallel and merge; Mutations → local-only unless [MutationOperation(Target = ...)] says otherwise (see below)
  • Local-only methods: Methods starting with GetStored or GetLibrary stay local-only in Combined wrapper

Where mutations go

By default every mutation (Add*/Create*/Update*/Upsert*/Delete*/Remove*, or anything marked [MutationOperation]) is applied to the local store only by the Fallback and Combined wrappers. That's right for client-owned data (a user's library), and wrong for server-owned per-user data (watch history, preferences) where the local copy is just a cache — a Combined client would happily "save" forever without the server ever hearing about it.

Set the target per method:

[GenerateApi(RoutePrefix = "/api/watchhistory")]
public interface IWatchHistoryRepository
{
    // API first, then mirrored locally so the next Combined read already sees it.
    // If the API call throws, it's logged as a warning and the local write still happens.
    [MutationOperation(Target = MutationTarget.Both)]
    Task UpsertEpisodeProgressAsync(string contentId, TimeSpan position, CancellationToken ct = default);

    // API only — the local store is never touched; exceptions propagate.
    [MutationOperation(Target = MutationTarget.Api)]
    Task PurgeAsync(CancellationToken ct = default);

    // Default (Local) — same as no attribute.
    Task AddToLibraryAsync(int showId, CancellationToken ct = default);
}
Target Fallback / Combined behaviour Failure handling
Local (default) _local only n/a
Api _api only exception propagates
Both _api then _local; a non-void method returns the API's result when it succeeded, else the local one API exception → ILogger.LogWarning, local write still runs

Requirements

  • .NET SDK with Roslyn source generators support
  • Refit (for the generated Refit interface)
  • Microsoft.Extensions.Logging (for Fallback/Combined wrappers)
There are no supported framework assets in this package.

Learn more about Target Frameworks and .NET Standard.

  • .NETStandard 2.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.5.1 169 10/10/2026
0.4.0 102 9/7/2026
0.3.1 96 9/6/2026
0.3.0 117 9/6/2026
0.2.0 125 4/19/2026
0.1.0 129 3/2/2026