Table of Contents

OpenAPI (Microsoft.AspNetCore.OpenApi)

OhData's core server package carries no OpenAPI dependency. To document the OData query parameters ($filter, $orderby, $top, $skip, $select, $expand, $count, $search) on collection endpoints when using ASP.NET Core's built-in AddOpenApi()/MapOpenApi() pipeline, install the EnGen.OhData.AspNetCore.OpenApi companion package.

dotnet add package EnGen.OhData.AspNetCore.OpenApi

Registration

The recommended one-liner is o.AddOhData(). It is the canonical wiring recipe — you do not need to know the transformer class names:

using OhData.AspNetCore.OpenApi;

builder.Services.AddOpenApi(o => o.AddOhData());

// ...

app.MapOpenApi();

This registers both the operation transformer (documents the OData query parameters) and the schema transformer (keeps generated schemas honest for profiles that use Ignore(...) — see below).

To also surface OhData's per-operation authorization in the document (#219/#220), pass the opt-in parameters — securitySchemeId emits an operation-level security requirement plus 401/403 responses referencing a scheme your app already defines, and authRequirements appends a human-readable requirements section to each secured operation's description:

builder.Services.AddOpenApi(o => o.AddOhData(
    authRequirements: AuthRequirementDisclosure.Kinds,
    securitySchemeId: "Bearer"));

Both default to off (null). See authorization.md for the auth-reflection boundary — OhData references the scheme by id but never defines it.

À la carte

Each transformer is independent. To register only one, call it directly instead of AddOhData():

builder.Services.AddOpenApi(o =>
{
    o.AddOperationTransformer<OhDataOpenApiOperationTransformer>();
    o.AddSchemaTransformer<OhDataOpenApiSchemaTransformer>();
});

The opt-in auth transformers have à la carte equivalents too — o.AddOperationTransformer(new OhDataOpenApiSecurityOperationTransformer("Bearer")) and o.AddOperationTransformer(new OhDataOpenApiAuthRequirementsOperationTransformer(AuthRequirementDisclosure.Kinds)).

What gets documented

OhDataOpenApiOperationTransformer implements IOpenApiOperationTransformer. For every endpoint that carries OhDataQueryOptionsMetadata (collection GET routes, $count, and GetById), it adds query parameters to the generated OpenAPI document driven by the entity set's capability flags:

Parameter Added when
$top / $skip Always, once per operation (paged collection endpoints)
$filter FilterEnabled
$orderby OrderByEnabled
$select SelectEnabled
$expand ExpandEnabled
$count CountEnabled
$search a Search handler is configured

The $top parameter's description includes the entity set's MaxTop value when one is configured, so consumers of the generated document see the server-enforced page-size cap.

The transformer is idempotent with respect to parameters another transformer may have already added under the same name (e.g. $top) - it will not add a duplicate.

Request bodies on write routes

Entity POST/PUT/PATCH, nav-POST, property PUT/PATCH, $ref POST/PUT, and bound/unbound actions all read and JSON-deserialize their request bodies by hand (see the "POST/PUT/PATCH deserialize the request body by hand" note in CLAUDE.md) rather than via a bound minimal-API parameter, so ApiExplorer sees no request body for them by default - no body editor, no schema, in any OpenAPI UI. AddOhData registers OhDataApiDescriptionProvider (an IApiDescriptionProvider) to fix this: it reads a plain OhDataRequestBodyMetadata marker attached to each write route at registration time and adds the corresponding body parameter/schema to the route's ApiDescription, which every OpenAPI document generator built on ApiExplorer - Microsoft.AspNetCore.OpenApi, NSwag, and Swashbuckle alike - then picks up automatically. No package installation or configuration is required beyond AddOhData itself; this part of docs-fidelity ships in the core package, unlike the query-parameter documentation above (which needs the doc-stack-specific companion package).

Body types documented: entity POST/PUT/PATCH use the model type (TModel); nav POST uses the navigation's item type; property PUT/PATCH and $ref POST/PUT use small documentation-only wrapper types (ODataPropertyWriteRequest<T> for {"value": ...}, ODataRefWriteRequest for {"@odata.id": "..."}); bound/unbound actions with parameters use a generic object schema, with the parameter names and CLR types listed in the body's description (an action's parameters are deserialized by name out of one JSON object, not bound to a single CLR type, so there is no single schema to generate).

Property routes omitted by default

Individual structural-property routes — GET /{Set}({key})/{Property}, its /$value variant, and the PUT/PATCH/DELETE property writes — are excluded from the generated document by default. They number up to four per property, per entity set, and would otherwise dominate the docs. This is applied at the ApiExplorer level (ExcludeFromDescription), so it covers this package, Swashbuckle, and NSwag identically, and it does not affect runtime behavior — the routes stay fully functional. Opt them back in per profile or server-wide via PropertyRouteDocsEnabled; see property-access.md.

Typed collection responses

Collection GET routes (on GetQueryable, GetAll, and Priority-1) and collection-valued navigation GET routes document their response as ODataCollectionResponse<T> - a small public DTO (@odata.context, @odata.count, @odata.nextLink, value) that mirrors the real OData collection envelope - instead of a bare, schema-less 200. It exists purely for documentation: the actual response is still built by hand as an ordered dictionary so annotations serialize before entity properties. $ref routes similarly document ODataRefResponse/ ODataRefCollectionResponse, and structural-property GET routes document ODataPropertyResponse<T>.

Schema property casing matches the wire

OhData owns its response JSON casing — PascalCase by default, independent of the host's HttpJsonOptions (see query-options.md → JSON property casing). OhDataOpenApiSchemaTransformer renames each generated schema property key to that same response casing, so the document advertises exactly what responses emit rather than the host serializer's casing (camelCase by ASP.NET Core default). A [JsonPropertyName] rename wins over the policy — in the schema and on the wire alike — matching the response precedence. Renaming is keyed by CLR model type (the same key the ignore suppression below uses).

Renaming follows the whole response graph, not just the top-level entity: nested complex types (a HomeAddress property, a List<Tag> collection, a dictionary value) and inherited base classes each get their own component schema, and every one of them is renamed to the response casing. The transformer drives that descent itself — because renaming a property key removes the host-cased key the runtime uses to locate a child schema, so left to its own traversal the runtime would stop at any renamed complex property and leave nested-only component schemas at host casing (#260).

Ignored properties omitted from schemas

Properties excluded via EntitySetProfile.Ignore(...) never cross the wire (see ignoring-properties.md), but OpenAPI schemas are generated from the CLR type — which still has the property. OhDataOpenApiSchemaTransformer implements IOpenApiSchemaTransformer and removes each ignored member from its model type's generated schema (request and response alike, since both share the component schema), so the document matches the real wire shape. Matching is by CLR member, immune to the naming policy — the profile ignores the CLR name (CostBasis), and the surviving keys are emitted in OhData's response casing (CostBasis by default; costBasis under a camelCase opt-in). Suppression is keyed by CLR model type, so a same-named property on a different (un-ignored) type is untouched.

Read-path summaries

Collection GET routes carry a WithSummary/WithDescription distinguishing which read path backs them: GetQueryable routes get "List {Set} (queryable)" with a description naming the live query options (driven by the profile's capability flags); GetAll routes get "List {Set} (simple read path)" with a description noting that $top/$skip/$select/$expand/$count are applied server-side post-materialization while $filter/$orderby are not supported. These flow through IEndpointSummaryMetadata/IEndpointDescriptionMetadata, which Microsoft.AspNetCore.OpenApi reads natively; the NSwag and Swashbuckle companion packages apply the same metadata explicitly (see their respective docs) since neither doc stack surfaces it automatically.

Same convention as the Swashbuckle companion

This package is the Microsoft.AspNetCore.OpenApi counterpart to EnGen.OhData.AspNetCore.Swashbuckle (see swashbuckle.md for the Swashbuckle IOperationFilter/ISchemaFilter equivalents). Both packages read the same OhDataQueryOptionsMetadata endpoint metadata and apply the same gating rules, so switching between the two OpenAPI generation pipelines does not change what gets documented.