API Versioning
OhData supports multiple simultaneous registrations with independent prefixes, EDM models, and profile sets. Each registration is completely isolated - no shared state.
Named registrations
builder.Services.AddOhData("v1", o => o
.WithPrefix("/v1")
.AddEntitySetProfile<ProductProfileV1>());
builder.Services.AddOhData("v2", o => o
.WithPrefix("/v2")
.AddEntitySetProfile<ProductProfileV1>()
.AddEntitySetProfile<CustomerProfileV2>()); // new entity set in v2
app.MapOhData("v1");
app.MapOhData("v2");
Each call produces its own EDM model and route group at its prefix:
GET /v1/Products ← v1 registration
GET /v2/Products ← v2 registration
GET /v2/Customers ← v2 only
Versioning convenience helpers
AddOhDataVersion and MapOhDataVersion are included in EnGen.OhData.AspNetCore and combine name and prefix into a single call:
// AddOhDataVersion / MapOhDataVersion live in Microsoft.Extensions.DependencyInjection /
// Microsoft.AspNetCore.Builder, so no OhData-specific using is required.
builder.Services.AddOhDataVersion("v1", "/v1", o => o.AddEntitySetProfile<ProductProfileV1>());
builder.Services.AddOhDataVersion("v2", "/v2", o => o
.AddEntitySetProfile<ProductProfileV1>()
.AddEntitySetProfile<CustomerProfileV2>());
app.MapOhDataVersion("v1");
app.MapOhDataVersion("v2");
OpenAPI / Swagger partitioning
Chain WithOpenApi() and WithGroupName() on the RouteGroupBuilder returned by MapOhData():
app.MapOhData("v1").WithOpenApi().WithGroupName("v1");
app.MapOhData("v2").WithOpenApi().WithGroupName("v2");
With Swashbuckle, add a DocInclusionPredicate so each endpoint appears in the correct doc. To
have Swagger UI also show the OData query parameters on each collection GET endpoint (driven by the
per-entity-set capability flags and MaxTop), call the one-line c.AddOhData() from the
EnGen.OhData.AspNetCore.Swashbuckle companion package inside the same
AddSwaggerGen call — it registers both the operation filter and the schema-fidelity filter. Both
read the same endpoint metadata regardless of which document an operation is partitioned into, so
they apply per document without extra configuration:
builder.Services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" });
c.SwaggerDoc("v2", new OpenApiInfo { Title = "My API", Version = "v2" });
c.DocInclusionPredicate((docName, apiDesc) =>
apiDesc.GroupName is null || apiDesc.GroupName == docName);
c.AddOhData();
});
See swashbuckle.md for the full filter setup, what gets documented, and the
schema-casing/Ignore(...) behavior.
Default (unnamed) registration
Calling AddOhData(...) without a name uses the key "__default__" internally and coexists cleanly with named registrations:
builder.Services.AddOhData(o => o.WithPrefix("/odata").AddEntitySetProfile<ProductProfile>());
builder.Services.AddOhData("v2", o => o.WithPrefix("/v2").AddEntitySetProfile<ProductProfileV2>());
app.MapOhData(); // maps __default__
app.MapOhData("v2"); // maps v2
Startup validation
Each registration independently validates for duplicate entity set names. Two profiles with the same EntitySetName within a single registration throw InvalidOperationException at startup. Duplicate names across different registrations are allowed.