b17s.Porta 0.4.0

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

b17s.Porta

CI NuGet License: MIT Docs

Porta is your way to create completely customizable, easy-to-implement, boiler-plate code free Backend-For-Frontend (BFF) services with opinionated, sensible defaults. Its architecture hooks into ASP.Net Core's minimal API design, and extends it via transformer-based API aggregation with multi-frontend/backend authentication support.

IMPORTANT Porta is not yet battle-hardened. For this reason it is advised to not use it on edge yet, but have a reverse proxy in front of it.

Features

  • Transformer Pattern: Clean separation between API contracts and backend calls
  • Multi-Backend Aggregation: Combine data from multiple backends in a single endpoint
  • Per-Backend Authentication: Configure different auth policies for each backend
  • Startup Validation: Catch configuration errors at application startup, not runtime
  • OIDC Endpoints: Opt-in login, logout, and back-channel logout middleware
  • Session Administration: Opt-in REST endpoints for session management
  • Raw Forwarding: Zero-code proxy endpoints for binary content, files, and non-JSON APIs
  • GraphQL Support: REST facade over GraphQL backends
  • OpenTelemetry: Automatic distributed tracing for all transformers and backend calls
  • Health Checks: Opt-in readiness checks for the IdP, session store, and Data Protection keys

Installation

dotnet add package b17s.Porta

Quick Start

Minimal Setup (Mapped Passthrough; No Auth)

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddPortaCore();

var app = builder.Build();

app.MapPassThrough<ProductsResponse>()
    .FromGet("/api/products")
    .ToGet("https://products-api.internal/products")
    .AllowAnonymous()
    .Build();

app.Run();

Note: endpoints require authorization by default (PortaCoreOptions.RequireAuthorizationByDefault = true). Drop .AllowAnonymous() only when you've configured an authentication scheme. Flip the option to false to invert the default if you'd rather opt into auth per endpoint.

With OIDC Authentication

Same pass-through endpoint as above, but now it requires a logged-in user and forwards their access token to the backend. The only new ingredients are the OIDC pipeline registration (AddPortaAuthentication, bound from configuration) and dropping .AllowAnonymous() in favour of .RequireAuth(). See the OIDC Endpoints guide for the login/logout middleware.

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddPortaCore(options => {
    options.TrustedHosts = ["https://products-api.internal"];
});

// Wire up the OIDC pipeline (cookie + OIDC handler + ticket store) from configuration.
builder.Services.AddPortaAuthentication(builder.Configuration);

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();

app.MapPassThrough<ProductsResponse>()
    .FromGet("/api/products")
    .ToGet("https://products-api.internal/products")
    .RequireAuth()                                      // 401 unless the user is logged in
    .WithBackendAuth(BackendAuthPolicies.BearerToken)   // forward the user's token to the backend
    .Build();

app.Run();

Raw Passthrough (Binary, Files, Non-JSON)

When you don't want to parse or reshape the response — file downloads, streaming, XML — use MapRawForward(). The body is streamed straight through; no response type is needed. See Raw Forwarding for header handling and size/timeout limits.

app.MapRawForward()
    .FromGet("/api/files/{id}")
    .ToGet("https://files-api.internal/files/{id}")
    .RequireAuth()
    .WithBackendAuth(BackendAuthPolicies.BearerToken)
    .Build();

Catch-all routes must use catch-all syntax on both sides. A plain {name} placeholder is encoded as a single path segment — any / in the value becomes %2F (secure by default, so a route value can't introduce new path segments). To proxy a multi-segment subtree, use {**name} in both the route pattern and the backend URL. Mixing them (catch-all route, plain backend placeholder) encodes the slashes and 404s at the backend:

app.MapRawForward()
    .FromGet("/files/{**rest}")
    .ToGet("https://files-api.internal/files/{**rest}")  // not {rest}
    .Build();
// /files/a/b/c.pdf  ->  https://files-api.internal/files/a/b/c.pdf

Introducing a Transformer

A transformer is where you reshape the backend response. Inherit a base class (here PassThroughTransformer<T>, overriding TransformResponse to strip an internal field), register it with AddTransformer<T>(), then map it with MapTransformer<,>(). See Transformers for the full hierarchy.

public class ProductsTransformer : PassThroughTransformer<ProductsResponse>
{
    // Strip the internal cost price the frontend should never see.
    protected override ProductsResponse TransformResponse(ProductsResponse response, TransformerContext context)
        => response with { Items = response.Items.Select(i => i with { CostPrice = null }).ToList() };
}
builder.Services.AddTransformer<ProductsTransformer>();

app.MapTransformer<ProductsTransformer, ProductsResponse>()
    .FromGet("/api/products")
    .ToGet("https://products-api.internal/products")
    .AllowAnonymous()
    .Build();

Documentation

Topic Description
Configuration Service registration, options, and production settings
Transformers Writing transformers, base classes, and error handling
Endpoints Endpoint builder, routing, and authorization
Authentication User auth providers and backend auth handlers
OIDC Endpoints Login, logout, back-channel logout, and session admin
Raw Forwarding Binary content, file proxying, and streaming
Caching Output caching endpoints, distributed (HA) cache, and per-leg aggregation caching
GraphQL Calling GraphQL backends from REST endpoints
Telemetry OpenTelemetry traces and metrics
Health Checks Readiness checks for the IdP, distributed cache, and Data Protection
HA Deployment Running multiple replicas behind a load balancer without sticky sessions
API Gateway Use When to use this library as a gateway, and when to pick something else
Advanced API versioning, MapGroup, OpenAPI metadata, and composing framework features

Try it (runnable demo)

A self-contained, run-out-of-the-box demo lives in demo/. One command brings up a complete BFF topology against two identity providers — Keycloak and Zitadel — orchestrated by .NET Aspire, plus end-to-end login tests against both:

cd demo
aspire run          # or: dotnet run --project Demo.AppHost

A container runtime (Docker Desktop or Podman) is the only prerequisite — the IdPs, database, and seeded test users are provisioned automatically. See demo/README.md for the topology, seeded credentials, exposed endpoints, and how to run the functional/E2E suite.

Transformer Hierarchy

Choose the right base class for your use case. Each links to its guide and a worked example:

Base Class Use Case Code Required
MapPassThrough<T>() Zero-code pass-through Config only
PassThroughTransformer<T> Simple pass-through 1 line
AuthenticatedTransformer<T> Auth required, no request body 1 line
AuthenticatedTransformer<TReq, TRes> Auth + backend request body 2-3 lines
AggregatingTransformer<T> Multi-backend aggregation ~15 lines
TransformerBase<T> Full custom control Varies

Backend Authentication

Built-in policies:

Policy Description Requires User
None No authentication No
BasicAuth HTTP Basic auth from config No
BearerToken Forward user's token Yes
TokenExchange Exchange for backend token (needs an audience - see Authentication) Yes
Custom Anything else (HMAC, API keys, client credentials) — implement IBackendAuthHandler Depends

Per-backend configuration:

app.MapTransformer<MyTransformer, Response>()
    .FromGet("/api/data")
    .ToBackends(b => b
        .ToGet("Users", "https://users.internal/api").WithAuth(BackendAuthPolicies.BearerToken)
        .ToGet("Products", "https://products.internal/api").WithAuth(BackendAuthPolicies.BasicAuth))
    .Build();

Custom Policies

The built-in policies aren't a closed set. Implement IBackendAuthHandler to add your own (HMAC signing, API keys, client-credentials, etc.). The handler exposes a PolicyName, and ApplyAuthAsync mutates the outgoing request — add headers, sign the body, attach a token. Register it with AddPortaAuthHandler<T>(), then reference it by name via WithBackendAuth("..."):

public class HmacAuthHandler : IBackendAuthHandler
{
    public string PolicyName => "HmacAuth";

    public Task ApplyAuthAsync(HttpRequestMessage request, BackendAuthContext context)
    {
        request.Headers.Add("X-Signature", ComputeHmacSignature(request));
        request.Headers.Add("X-Timestamp", DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString());
        return Task.CompletedTask;
    }
}

// Registration
builder.Services.AddPortaAuthHandler<HmacAuthHandler>();

// Usage — reference the custom handler by its PolicyName
app.MapPassThrough<Response>()
    .FromGet("/api/partner-data")
    .ToGet("https://partner-api.example.com/data")
    .WithBackendAuth("HmacAuth")
    .Build();

See Authentication for the full BackendAuthContext contract and registering multiple handlers.

Product Compatible and additional computed target framework versions.
.NET 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.4.0 106 7/6/2026
0.3.1-rc.6 68 6/24/2026
0.3.0-rc.5 79 6/17/2026
0.2.1-rc.4 71 6/16/2026
0.2.0-rc.3 67 6/15/2026
0.1.0-rc.2 61 6/14/2026