AndersonN.Omni.AutoApi.AspNetCore 0.2.0

dotnet add package AndersonN.Omni.AutoApi.AspNetCore --version 0.2.0
                    
NuGet\Install-Package AndersonN.Omni.AutoApi.AspNetCore -Version 0.2.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="AndersonN.Omni.AutoApi.AspNetCore" Version="0.2.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="AndersonN.Omni.AutoApi.AspNetCore" Version="0.2.0" />
                    
Directory.Packages.props
<PackageReference Include="AndersonN.Omni.AutoApi.AspNetCore" />
                    
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 AndersonN.Omni.AutoApi.AspNetCore --version 0.2.0
                    
#r "nuget: AndersonN.Omni.AutoApi.AspNetCore, 0.2.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 AndersonN.Omni.AutoApi.AspNetCore@0.2.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=AndersonN.Omni.AutoApi.AspNetCore&version=0.2.0
                    
Install as a Cake Addin
#tool nuget:?package=AndersonN.Omni.AutoApi.AspNetCore&version=0.2.0
                    
Install as a Cake Tool

Omni.AutoApi

CI NuGet Downloads License: MIT

Mecanismo estilo ABP Auto API Controllers para ASP.NET Core (.NET 10): exponha Application Services como controllers HTTP automaticamente e consuma-os por clientes tipados — um proxy dinâmico em runtime ou um cliente real gerado em tempo de compilação. A mesma interface roda no servidor, no Blazor (Server/WebAssembly) e no MAUI.

dotnet add package AndersonN.Omni.AutoApi.AspNetCore
builder.Services.AddAutoApiServices();     // pronto: seus IRemoteService viram endpoints
app.MapControllers();

Destaques

  • Controllers automáticos a partir de IRemoteService — sem [ApiController]/[Route]/[HttpGet].
  • Clientes tipados em duas formas: proxy dinâmico (runtime, DispatchProxy) e cliente real gerado (compile-time, AOT-friendly) — ambos derivam a rota da mesma fonte, então não saem de sincronia.
  • Extensões de DI geradas: Add{Nome}Client(...) (via IHttpClientFactory quando disponível) e AddAllAutoApiClients(...) — incl. overload com configuração por serviço (multi-backend).
  • Validação automática: DataAnnotations inválidas → 400 ProblemDetails com erros por campo.
  • [Authorize] declarativo nos Application Services (metadata preservado no endpoint).
  • Upload via RemoteStreamContent (multipart) e streaming via IAsyncEnumerable<T> — nos dois clientes.
  • Base ApplicationService enriquecida: Logger, CurrentUser, GetRequiredService<T> sem construtor.
  • Erros padronizados em ProblemDetails (RFC 9457) + exceções de negócio.
  • Endpoint de definição /api/auto-api/definition (estilo ABP /api/abp/api-definition).
  • Rotas configuráveis (RouteOptions) e versionamento opt-in (Asp.Versioning).
  • Coberto por 80 testes (unit + integração). Licença MIT.

Pacotes

Pacote (NuGet) Descrição TFM
AndersonN.Omni.AutoApi.Abstractions IRemoteService, atributos, RemoteStreamContent e regras de rota/verbo net9.0; net10.0
AndersonN.Omni.AutoApi.AspNetCore Lado servidor: transforma IRemoteService em controllers MVC reais net9.0; net10.0
AndersonN.Omni.AutoApi.Client Lado cliente: proxy HTTP dinâmico (runtime, via DispatchProxy) + DI net9.0; net10.0
AndersonN.Omni.AutoApi.Client.SourceGenerator Gera o cliente HTTP real (XxxClient) + extensões de DI em compilação netstandard2.0 (analyzer)
dotnet add package AndersonN.Omni.AutoApi.AspNetCore   # servidor
dotnet add package AndersonN.Omni.AutoApi.Client       # cliente (proxy dinâmico)

Nome do pacote × namespace: os IDs no NuGet usam o prefixo AndersonN. (o prefixo Omni.* é reservado por outra conta no nuget.org), mas os namespaces C# continuam Omni.AutoApi.* — ou seja, você instala AndersonN.Omni.AutoApi.Client e escreve using Omni.AutoApi.Client;.

O pacote ...Client.SourceGenerator é um analisador e requer ...Abstractions no projeto consumidor (vem transitivamente ao usar ...AspNetCore ou ...Client).

Início rápido

Servidor — expondo um Application Service

public class TodoApplicationService : ApplicationService, ITodoAppService   // ApplicationService : IRemoteService
{
    public Task<List<TodoItem>> GetTodosAsync() => ...;          // GET    api/app-service/todo/get-todos
    public Task<TodoItem>       GetTodoAsync(int id) => ...;      // GET    api/app-service/todo/get-todo?id=
    public Task<TodoItem>       CreateTodoAsync(CreateTodoDto i); // POST   api/app-service/todo/create-todo  (body)
    public Task<TodoItem>       UpdateTodoAsync(int id, UpdateTodoDto i); // PUT  ...update-todo?id=  (body)
    public Task                 DeleteTodoAsync(int id);          // DELETE ...delete-todo?id=  -> 204
}
builder.Services.AddAutoApiServices();   // descobre IRemoteService, cria controllers, enriquece o OpenAPI
...
app.MapControllers();
app.MapAutoApiDefinition();               // (opcional) /api/auto-api/definition

Usando o serviço in-process (Blazor Server, jobs, testes) — além do endpoint HTTP:

builder.Services.AddAutoApiServer<ITodoAppService, TodoApplicationService>();
// ou, para todos os IRemoteService de um assembly:
builder.Services.AddAutoApiServers(typeof(TodoApplicationService).Assembly);

Prefira isso a um AddScoped manual: além de registrar, ele injeta o LazyServices, então Logger/CurrentUser/GetRequiredService funcionam igual no caminho in-process e no HTTP (com AddScoped puro, Logger viraria NullLogger silenciosamente e CurrentUser lançaria). Também garante que o assembly do serviço seja varrido, caso ele more numa class library.

Convenções: verbo pelo prefixo do método (Get/Create/Update/Delete/Patch), rota kebab-case, tipos simples → query, DTO complexo → body (exceto GET/DELETE). [Http*]/[Route]/[From*] explícitos são respeitados, e colisões de rota (sobrecargas) falham no startup com mensagem clara.

Cliente — mesma interface, registro por host

[AutoApiClient]                                   // habilita o cliente gerado
public interface ITodoAppService : IRemoteService { /* mesmos métodos */ }

(1) Proxy dinâmico (runtime, sem geração):

builder.Services.AddAutoApiClient<ITodoAppService>((_, c) => c.BaseAddress = new Uri(baseUrl));
// ou descobrindo todas as interfaces IRemoteService de um assembly:
builder.Services.AddAutoApiClients(typeof(ITodoAppService).Assembly, (_, c) => c.BaseAddress = new Uri(baseUrl));

(2) Cliente gerado (compile-time, AOT-friendly): o [AutoApiClient] faz o source generator emitir TodoAppServiceClient : ITodoAppService e extensões de DI (using Omni.AutoApi.Client.Generated;):

builder.Services.AddTodoAppServiceClient((_, http) => http.BaseAddress = new Uri(baseUrl)); // um cliente
builder.Services.AddAllAutoApiClients((_, http) => http.BaseAddress = new Uri(baseUrl));     // todos do assembly

// multi-backend: configuração POR SERVIÇO (o callback recebe o Type da interface):
builder.Services.AddAllAutoApiClients((_, http, svc) =>
    http.BaseAddress = new Uri(svc == typeof(IOrderAppService) ? ordersUrl : defaultUrl));

Quando Microsoft.Extensions.Http está presente, Add{Nome}Client usa AddHttpClient + typed client e retorna IHttpClientBuilder — encadeie resiliência e handlers (auth, correlação):

builder.Services.AddTodoAppServiceClient((_, http) => http.BaseAddress = new Uri(baseUrl))
    .AddHttpMessageHandler<AuthTokenHandler>()      // DelegatingHandler p/ Authorization: Bearer ...
    .AddStandardResilienceHandler();                 // Microsoft.Extensions.Http.Resilience

Recursos do servidor

Base ApplicationService enriquecida — helpers resolvidos sob demanda (injetados por um filtro do MVC, sem substituir o ativador de controllers), sem construtor:

public Task<List<TodoItem>> GetTodosAsync()
{
    Logger.LogInformation("user={User}", CurrentUser.Id);   // ILogger + ICurrentUser via LazyServices
    var repo = GetRequiredService<ITodoRepository>();
    ...
}

Pipeline de erro — exceções viram ProblemDetails (RFC 9457) com status mapeado; mensagens de negócio são preservadas, as de framework (4xx) são mascaradas:

throw new EntityNotFoundException("Todo não encontrado");  // -> 404 { "code": "EntityNotFound", ... }
throw new BusinessException("Saldo insuficiente");         // -> 409

Validação automática — DataAnnotations no DTO ([Required], [Range], ...) inválidas retornam 400 ProblemDetails com "code": "ValidationError" e erros por campo, sem código na action.

Autenticação e autorização — [Authorize]/policies na classe ou método do Application Service funcionam normalmente (o metadata é preservado no endpoint). No servidor, nada além do setup padrão de auth do ASP.NET Core:

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(/* ... */);
builder.Services.AddAuthorization(o => o.AddPolicy("todo:admin", p => p.RequireRole("admin")));
// ...
app.UseAuthentication();
app.UseAuthorization();
public class TodoApplicationService : ApplicationService, ITodoAppService
{
    public Task<List<TodoItem>> GetTodosAsync() => ...;              // público

    [Authorize]                                                       // exige token válido -> 401
    public Task<TodoItem> CreateSecureTodoAsync(CreateTodoDto input) => ...;

    [Authorize(Policy = "todo:admin")]                                // exige a policy -> 403
    public Task DeleteAllTodosAsync() => ...;
}

Do lado cliente, use o AuthTokenHandler para anexar Authorization: Bearer … (o callback é chamado a cada requisição, então serve tanto para token fixo quanto para renovação):

builder.Services.AddSingleton(sp => new AuthTokenHandler(() => sp.GetRequiredService<ITokenStore>().Current));
builder.Services.AddTodoAppServiceClient((_, http) => http.BaseAddress = new Uri(baseUrl))
                .AddHttpMessageHandler<AuthTokenHandler>();

O sample Omni.AutoApi.Sample.Web traz o fluxo JWT completo (emissor de token de teste em /dev/token, serviço protegido e policy), coberto por testes de integração 200/401/403.

Upload e streaming — a mesma interface declara upload (sem depender de IFormFile) e streaming incremental, funcionando no servidor e nos dois clientes:

Task<string> CreateAttachmentAsync(RemoteStreamContent content);         // multipart/form-data
IAsyncEnumerable<TodoItem> GetTodoStreamAsync(CancellationToken ct = default); // JSON incremental

Rotas configuráveis (RouteOptions) — no servidor e no gerador (mantenha os dois em sincronia):

builder.Services.AddAutoApiServices(o => { o.Prefix = "api/services"; o.UseKebabCase = true; });

<PropertyGroup>
  <AutoApiRoutePrefix>api/services</AutoApiRoutePrefix>
  <AutoApiControllerPostfixes>AppService;Handler</AutoApiControllerPostfixes>
</PropertyGroup>

Versionamento opt-in — lê a versão por query (?api-version=2.0) ou header (X-Api-Version), sem mudar a rota:

builder.Services.AddAutoApiVersioning();
[ApiVersion("1.0")]
[ApiVersion("2.0")]
public class CatalogAppService : ApplicationService
{
    public Task<string> GetNameAsync() => ...;                       // nas duas versões

    [MapToApiVersion("1.0")] public Task<string> GetLegacyCodeAsync() => ...;   // só v1
    [MapToApiVersion("2.0")] public Task<Summary> GetSummaryAsync() => ...;     // só v2
}

Um documento OpenAPI por versão — DiscoverApiDocumentNames lê os [ApiVersion] e devolve ["v1", "v2"], então a biblioteca não precisa depender de um pacote de OpenAPI específico (o mesmo laço serve para AddOpenApi nativo, Swashbuckle ou NSwag):

foreach (var doc in ApiVersioningExtensions.DiscoverApiDocumentNames())
{
    builder.Services.AddOpenApi(doc);      // /openapi/v1.json e /openapi/v2.json
}

O endpoint de definição também fica ciente de versão: cada ação traz apiVersion, o documento lista apiVersions, e ?apiVersion=v2 filtra a definição para gerar um cliente só da v2.

Para ajustar o ApiExplorer (ex.: formato do nome do documento), use o options pattern: services.Configure<ApiExplorerOptions>(o => o.GroupNameFormat = "'v'VV");

Diagnósticos em tempo de compilação

O pacote AspNetCore traz um analisador que antecipa para a digitação erros que só apareceriam no startup — sem instalar nada a mais:

ID Severidade O que pega
AUTOAPI002 Warning Sobrecarga (ou par Foo/FooAsync) que gera rota duplicada — hoje derruba a aplicação ao iniciar
AUTOAPI003 Warning Mais de um parâmetro complexo em verbo com corpo — o MVC só aceita um [FromBody]
AUTOAPI004 Info Método não assíncrono: o endpoint funciona, mas os clientes tipados não o implementam
public class TodoAppService : ApplicationService
{
    public Task<Todo> GetTodoAsync(int id) => ...;
    public Task<Todo> GetTodoAsync(string slug) => ...;   // ⚠ AUTOAPI002: mesma rota GET /get-todo
}

Do lado cliente, o AUTOAPI001 (source generator) já sinaliza métodos que ele não consegue gerar. Para ajustar a severidade, use o .editorconfig: dotnet_diagnostic.AUTOAPI004.severity = none.

Exemplos (samples/)

Exemplo O que mostra
Omni.AutoApi.Sample.Web Host + consumidor básico; OpenAPI/Scalar; pipeline de erro.
BlazorMauiAuto Blazor Web App (InteractiveAuto: Server → WebAssembly) + MAUI compartilhando páginas, com a Omni.AutoApi como camada de dados (o mesmo ITodoAppService nos três hosts).
Distribution Distribuir o cliente gerado para outra solution via pacote de contratos (o cliente vem "assado" no DLL; o consumidor não precisa do gerador).

Estrutura do repositório

src/
  Omni.AutoApi.Abstractions/           # tipos compartilhados (fonte de verdade das rotas)
  Omni.AutoApi.AspNetCore/             # mecanismo server (convention, providers, DI, erro, versioning)
  Omni.AutoApi.Client/                 # proxy dinâmico de runtime + DI
  Omni.AutoApi.Client.SourceGenerator/ # gerador Roslyn (cliente + extensões de DI)
samples/
  Omni.AutoApi.Sample.Web/             # exemplo básico
  BlazorMauiAuto/                 # Blazor Auto + MAUI compartilhando UI/dados
  Distribution/                   # distribuição cross-solution via NuGet
tests/
  Omni.AutoApi.Tests/                  # unit (ApiRouteBuilder, TypeHelper, gerador)
  Omni.AutoApi.IntegrationTests/       # WebApplicationFactory (e2e)

Build / testes / empacotamento

dotnet build Omni.AutoApi.sln
dotnet test  Omni.AutoApi.sln
dotnet pack  Omni.AutoApi.sln -c Release -o artifacts   # gera os 4 .nupkg

Publicação

A publicação no nuget.org é automática via Trusted Publishing (OIDC — sem API key de longa duração). Basta criar a tag; o workflow release.yml usa a tag como versão dos pacotes:

git tag v0.2.0 && git push origin v0.2.0   # publica Omni.AutoApi.* 0.2.0

Limitações & notas

  • Sobrecargas de método não são suportadas (colisão de rota → falha no startup); use [HttpGet("rota")]/[Route].
  • Cliente gerado suporta Task/ValueTask(<T>) e IAsyncEnumerable<T>. Métodos genéricos, com mais de um parâmetro complexo (corpo) ou com Stream/IFormFile crus (use RemoteStreamContent) viram stub que lança NotSupportedException + diagnóstico AUTOAPI001.
  • Query string: contrato de serialização é ISO-8601 invariante para datas/horas (DateOnly → 2026-06-24) e nome para enums. DTOs complexos em GET/DELETE são achatados em 1 nível (propriedades complexas aninhadas são omitidas).
  • Tratamento de erro no cliente: chamam EnsureSuccessStatusCode() e propagam exceções (HttpRequestException/JsonException); aplique resiliência via IHttpClientBuilder retornado pelo registro gerado (.AddStandardResilienceHandler()).
  • Sincronia de contrato: rota/verbo derivam do mesmo ApiRouteBuilder, mas a assinatura não é validada contra o servidor — compartilhe a mesma interface (idealmente num assembly de contratos).
  • Dinâmico vs. gerado: o proxy dinâmico usa DispatchProxy + reflexão (zero setup); o gerado é código real (mais rápido, debugável, AOT-friendly). Mesma semântica de rota/binding.
  • Trimming/AOT: o caminho reflexivo do cliente (DTO complexo em GET/DELETE) já é anotado com [DynamicallyAccessedMembers]; ainda assim prefira parâmetros simples/body em apps trimmed.
  • BaseAddress: normalizada automaticamente (barra final); ausência gera erro claro na 1ª chamada.

Contribuindo

Veja CONTRIBUTING.md (build, testes, política de API pública) e SECURITY.md para reporte de vulnerabilidades.

Licença, changelog & roadmap

MIT. Histórico em CHANGELOG.md. Próximos passos e pendências priorizadas em ROADMAP.md.

Product Compatible and additional computed target framework versions.
.NET net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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.2.0 187 8/8/2026
0.1.0 122 7/25/2026

Consulte o CHANGELOG.md do repositório.