Skip to main content
ErrorOrX provides full support for ASP.NET Core API versioning through the Asp.Versioning.Http package. The generator automatically emits version sets and applies them to endpoints.
API versioning support was added in v3.1.0. Make sure you have the latest version of ErrorOrX.Generators.

Quick Start

using Asp.Versioning;
using ErrorOrX.Attributes;

[ApiVersion(1.0)]
[ApiVersion(2.0)]
public static class TodoApi
{
    [Get("/api/todos/{id}")]
    [MapToApiVersion(1.0)]
    public static ErrorOr<TodoV1> GetV1(Guid id) => new TodoV1(id, "Task");

    [Get("/api/todos/{id}")]
    [MapToApiVersion(2.0)]
    public static ErrorOr<TodoV2> GetV2(Guid id, TimeProvider time)
        => new TodoV2(id, "Task", time.GetUtcNow());
}

Installation

dotnet add package Asp.Versioning.Http
If you use versioning attributes without the Asp.Versioning.Http package referenced, the generator emits EOE052 warning.

Attributes

[ApiVersion]

Declares which API versions an endpoint class or method supports.
// All endpoints in this class support v1.0 and v2.0
[ApiVersion(1.0)]
[ApiVersion(2.0)]
public static class MyApi { }

[MapToApiVersion]

Maps a specific endpoint to a specific version. Required when multiple endpoints share the same route but serve different versions.
[ApiVersion(1.0)]
[ApiVersion(2.0)]
public static class ProductApi
{
    // Both endpoints have the same route, distinguished by version
    [Get("/api/products")]
    [MapToApiVersion(1.0)]
    public static ErrorOr<List<ProductV1>> ListV1() => ...

    [Get("/api/products")]
    [MapToApiVersion(2.0)]
    public static ErrorOr<List<ProductV2>> ListV2() => ...
}

[ApiVersionNeutral]

Marks an endpoint as version-neutral (available on all versions). Use this for infrastructure endpoints like health checks.
[ApiVersionNeutral]
[Get("/health")]
public static ErrorOr<string> HealthCheck() => "OK";
[ApiVersionNeutral] and [MapToApiVersion] are mutually exclusive. Using both triggers EOE050 warning.

Version Formats

ErrorOrX supports multiple version formats:
FormatExampleDescription
Numeric[ApiVersion(1.0)]Major.minor numeric version
Major only[ApiVersion(2)]Major version only
String with status[ApiVersion("1.0-beta")]Version with pre-release status
Preview[ApiVersion("2.0-preview.1")]Preview version with iteration
// Valid formats
[ApiVersion(1.0)]
[ApiVersion(2.5)]
[ApiVersion("1.0-beta")]
[ApiVersion("2.0-preview.1")]

// Invalid formats (trigger EOE054)
[ApiVersion("v1")]        // No "v" prefix
[ApiVersion("1.0.0.0")]   // Too many segments
[ApiVersion("latest")]    // Not a version number

Generated Code

The generator emits version sets and applies them to endpoints:
// Generated version set (created once per unique version combination)
private static readonly ApiVersionSet __vs1 = app.NewApiVersionSet()
    .HasApiVersion(new ApiVersion(1, 0))
    .HasApiVersion(new ApiVersion(2, 0))
    .Build();

// Applied to endpoint
app.MapGet("/api/todos/{id}", Invoke_Ep1)
    .WithApiVersionSet(__vs1)
    .MapToApiVersion(new ApiVersion(1, 0));

Version-Neutral Endpoints

// Generated for [ApiVersionNeutral]
app.MapGet("/health", Invoke_Ep_Health)
    .WithApiVersionSet(__vsNeutral);

private static readonly ApiVersionSet __vsNeutral = app.NewApiVersionSet()
    .IsApiVersionNeutral()
    .Build();

Deprecated Versions

// Mark a version as deprecated
[ApiVersion(1.0, Deprecated = true)]
[ApiVersion(2.0)]
public static class LegacyApi { }

// Generated
private static readonly ApiVersionSet __vs2 = app.NewApiVersionSet()
    .HasDeprecatedApiVersion(new ApiVersion(1, 0))
    .HasApiVersion(new ApiVersion(2, 0))
    .Build();

Service Registration

Register API versioning in your Program.cs:
var builder = WebApplication.CreateBuilder(args);

// Add API versioning
builder.Services.AddApiVersioning(options =>
{
    options.DefaultApiVersion = new ApiVersion(1, 0);
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.ReportApiVersions = true;
    options.ApiVersionReader = ApiVersionReader.Combine(
        new UrlSegmentApiVersionReader(),
        new HeaderApiVersionReader("X-Api-Version"),
        new QueryStringApiVersionReader("api-version"));
});

// Add ErrorOr endpoints
builder.Services.AddErrorOrEndpoints();

var app = builder.Build();
app.MapErrorOrEndpoints();
app.Run();

Version Reader Options

ReaderUsage
UrlSegmentApiVersionReader/api/v1/todos
HeaderApiVersionReaderX-Api-Version: 1.0
QueryStringApiVersionReader?api-version=1.0

Diagnostics

IDSeverityDescription
EOE050Warning[ApiVersionNeutral] and [MapToApiVersion] are mutually exclusive
EOE051Warning[MapToApiVersion] references version not declared with [ApiVersion]
EOE052WarningAsp.Versioning.Http package not referenced
EOE053InfoEndpoint missing versioning when others use it
EOE054ErrorInvalid API version format
See Diagnostics for detailed examples and fixes.

OpenAPI Integration

Versioned endpoints automatically include version metadata in OpenAPI documentation:
{
  "paths": {
    "/api/todos/{id}": {
      "get": {
        "tags": ["TodoApi"],
        "operationId": "TodoApi_GetV1",
        "x-api-version": "1.0"
      }
    }
  }
}

Best Practices

1

Declare versions at class level

When all endpoints in a class share the same versions, declare them once at the class level.
2

Use MapToApiVersion sparingly

Only use [MapToApiVersion] when multiple endpoints share the same route but serve different versions.
3

Mark infrastructure as version-neutral

Use [ApiVersionNeutral] for health checks, metrics, and other infrastructure endpoints.
4

Deprecate before removing

Mark old versions as deprecated before removing them to give consumers time to migrate.

Complete Example

using Asp.Versioning;
using ErrorOrX.Attributes;

[ApiVersion(1.0)]
[ApiVersion(2.0)]
public static class OrderApi
{
    // Available on v1 only
    [Get("/api/orders")]
    [MapToApiVersion(1.0)]
    public static ErrorOr<List<OrderV1>> ListV1(IOrderService svc)
        => svc.GetAllV1();

    // Available on v2 only (breaking change: pagination required)
    [Get("/api/orders")]
    [MapToApiVersion(2.0)]
    public static ErrorOr<PagedResult<OrderV2>> ListV2(
        IOrderService svc,
        [FromQuery] int page = 1,
        [FromQuery] int pageSize = 20)
        => svc.GetPagedV2(page, pageSize);

    // Available on both versions (no MapToApiVersion)
    [Get("/api/orders/{id}")]
    public static ErrorOr<Order> GetById(Guid id, IOrderService svc)
        => svc.GetById(id);
}

// Health check: version-neutral
[ApiVersionNeutral]
public static class HealthApi
{
    [Get("/health")]
    public static ErrorOr<HealthStatus> Check() => new HealthStatus("Healthy");
}