ASP.NET Core 11 OpenAPI deprecation now carries C# [Obsolete] metadata into the generated API contract. Mark an endpoint handler, response type, or property as obsolete and ASP.NET Core emits deprecated: true for the matching OpenAPI operation, schema, or schema property. That removes the custom transformer previously needed to keep source code and client-facing documentation aligned.

The practical upgrade task is not just to add attributes. First inventory the obsolete surface, generate the document, and approve the exact deprecation paths in source control. Then make CI fail when a deprecation flag appears or disappears unexpectedly. The flag is contract metadata: it does not remove an endpoint, change its HTTP behavior, or guarantee that every client generator will treat it as an error.

What ASP.NET Core 11 changes

Before .NET 11, an API could warn C# callers through ObsoleteAttribute while its OpenAPI document still described the same operation or model as current. Teams had to add an IOpenApiOperationTransformer or IOpenApiSchemaTransformer to copy that intent into the contract.

ASP.NET Core 11 RC1 makes the mapping automatic. The built-in generator detects [Obsolete] on a Minimal API handler, an MVC action, endpoint metadata, a schema type, or a schema property. It then sets the corresponding OpenAPI deprecated value to true. The implementation was merged on July 22, 2026 and is documented in the .NET 11 RC1 release notes.

  • Operation: the HTTP operation receives deprecated: true.
  • Schema type: the component schema receives deprecated: true.
  • Inline property: the property schema receives deprecated: true.
  • Referenced property: the deprecation is attached to the reference use, not blindly copied onto the shared component type.

This is a contract-generation change, not an endpoint-lifecycle system. The old route remains callable until your application removes or changes it. The ObsoleteAttribute message is useful to .NET developers, but OpenAPI exposes only the standardized deprecation flag; publish replacement guidance separately in descriptions, release notes, or your API portal.

Mark operations, types, and properties deliberately

A Minimal API handler can carry the operation-level attribute while its return model marks the type and one property independently. Keeping those decisions separate matters because an obsolete route does not always return an obsolete model, and an obsolete property does not necessarily deprecate every operation that returns its containing type.

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenApi();

var app = builder.Build();
app.MapOpenApi();

app.MapGet("/catalog/{id}", GetCatalogItem);

#pragma warning disable CS0618 // Mapped intentionally during the migration window.
app.MapGet("/catalog/legacy/{id}", GetLegacyCatalogItem);
#pragma warning restore CS0618

static CatalogItem GetCatalogItem(int id) =>
    new(id, $"Product {id}", $"SKU-{id:D4}");

[Obsolete("Use GET /catalog/{id}.")]
static LegacyCatalogItem GetLegacyCatalogItem(int id) =>
    new(id, $"Product {id}", $"SKU-{id:D4}");

public sealed record CatalogItem(
    int Id,
    string Name,
    string StockKeepingUnit);

[Obsolete("Use CatalogItem.")]
public sealed record LegacyCatalogItem(
    int Id,
    string Name,
    [property: Obsolete("Use StockKeepingUnit.")] string Sku);

app.Run();

The narrow #pragma suppresses the expected compiler warning at the intentional mapping site. Do not disable CS0618 for the whole project: other calls to the obsolete API should still warn. For an endpoint built through a lambda or where the handler method cannot carry the attribute conveniently, attach new ObsoleteAttribute(...) with WithMetadata.

app.MapGet("/catalog/legacy-summary", () => Results.Ok(new { count = 0 }))
   .WithMetadata(new ObsoleteAttribute(
       "Use GET /catalog with the summary query option."));

MVC actions follow the same intent: place [Obsolete] on the action method that represents the deprecated operation. Avoid putting it on a broad controller base class and assuming every derived action will inherit the contract flag. The implementation deliberately performs type-level checks without inherited attributes.

Verify ASP.NET Core 11 OpenAPI deprecation

Inspect the generated document before treating the migration as complete. With the default document name, the development endpoint is commonly available at /openapi/v1.json. An integration test can request that document and assert the exact operation, component schema, and property paths that your team intends to deprecate.

public sealed class OpenApiDeprecationTests(
    WebApplicationFactory<Program> factory)
    : IClassFixture<WebApplicationFactory<Program>>
{
    [Fact]
    public async Task Legacy_contract_surface_is_deprecated()
    {
        using var client = factory.CreateClient();
        var json = await client.GetStringAsync("/openapi/v1.json");
        var document = JsonNode.Parse(json)!;

        Assert.True((bool?)document["paths"]!["/catalog/legacy/{id}"]!
            ["get"]!["deprecated"]);

        Assert.True((bool?)document["components"]!["schemas"]!
            ["LegacyCatalogItem"]!["deprecated"]);

        Assert.True((bool?)document["components"]!["schemas"]!
            ["LegacyCatalogItem"]!["properties"]!["sku"]!["deprecated"]);
    }
}

This test verifies your generated contract, not only framework behavior in isolation. It catches a route-name change, a schema-name change, an environment that omits OpenAPI, or an application transformer that overrides the built-in value. Configure the test host so MapOpenApi is active in the test environment; do not expose an internal document publicly merely to make the test pass.

Also test a current operation or model with Assert.False or a null check. Positive assertions prove intended flags exist, while a negative control catches a transformer or convention that marks too much of the API as deprecated.

Turn the generated contract into a CI gate

Failing CI whenever any deprecation exists would prevent a deliberate migration. Instead, store a small approved inventory of deprecation paths. CI should compare the current generated document with that inventory and stop on both unexpected additions and unexpected removals.

jq -S '
  [paths(scalars) as $path
   | select($path[-1] == "deprecated" and getpath($path) == true)
   | $path | map(tostring) | join(".")]
  | sort
' artifacts/openapi.json \
  > artifacts/openapi-deprecations.current.json

diff --unified \
  contracts/openapi-deprecations.approved.json \
  artifacts/openapi-deprecations.current.json

The approved file is intentionally smaller than a full OpenAPI snapshot. A complete contract diff is valuable for broader compatibility checks, but it can hide the deprecation decision inside unrelated description, ordering, or schema changes. Keep the focused inventory reviewable in the same pull request that introduces or removes [Obsolete].

  1. Generate the document from the same application configuration CI normally validates.
  2. Extract every path whose final field is deprecated with the value true.
  3. Sort the list to remove ordering noise.
  4. Compare it with the approved inventory committed to the repository.
  5. Require the pull request to explain every intentional inventory change and name the replacement path or model.

Do not update the approved file automatically after a mismatch. That turns the gate into a recorder and allows accidental contract drift to pass without review. Produce the current inventory as a build artifact so the author can inspect and deliberately accept the change.

Handle referenced properties and overrides

OpenAPI schemas can reuse a shared component through a reference. If one property is obsolete only where that reference is used, marking the underlying component as deprecated would incorrectly affect every consumer of the shared type. ASP.NET Core 11 handles this case by carrying reference-specific deprecation metadata and applying it to the OpenApiSchemaReference.

This distinction is a reason to assert the final JSON path rather than only searching the document text for "deprecated": true. The generated representation can differ between an inline property, a reusable component, and a reference use, while the consumer-facing result should remain accurate.

The automatic behavior is on by default, but an application can override a particular value in an IOpenApiOperationTransformer or IOpenApiSchemaTransformer. Treat that as an exception requiring a reason. Setting Deprecated = false can be justified when source-level obsolescence serves an internal C# migration that does not represent an HTTP-contract deprecation, but it creates a deliberate difference between two audiences and deserves a test.

Roll out deprecations without surprising consumers

Different documentation tools and client generators react differently to OpenAPI deprecation metadata. Some display a badge or warning, some annotate generated members, and some may not surface the flag prominently. Therefore, validate the actual generator and version used by important consumers before relying on the flag as the only notification channel.

  1. Inventory: list obsolete operations, models, and properties before upgrading to ASP.NET Core 11.
  2. Generate: inspect the first upgraded document for newly emitted flags.
  3. Communicate: publish the supported replacement and a realistic removal window.
  4. Verify consumers: regenerate representative clients and review warnings, public API shape, and build behavior.
  5. Gate: commit the approved deprecation inventory and require review for changes.
  6. Observe: use normal API telemetry to determine whether the legacy route is still used; the OpenAPI flag does not measure adoption.
  7. Remove separately: delete the old surface only after the compatibility policy and usage evidence permit it.

If the upgrade reveals flags you are not ready to expose, the safest rollback is to keep the older framework/package version while you reconcile the contract. A targeted transformer override is possible, but it should not become a silent blanket that hides all obsolescence.

Production risks and edge cases

  • Upgrade drift: existing [Obsolete] attributes can add contract flags immediately after the framework upgrade even when no endpoint code changed.
  • Generator behavior: deprecated: true is standardized metadata, but downstream tools decide whether it becomes a warning, annotation, or no visible change.
  • No removal semantics: the flag does not return 410 Gone, block requests, or create a sunset deadline.
  • Message loss: the C# attribute message is not a replacement-document field. Keep migration guidance in a client-visible location.
  • Schema reuse: check whether deprecation belongs to a shared type, an inline property, or one reference use before approving the generated path.
  • Environment differences: build-time or test-time OpenAPI must use the same endpoint registration and transformers as the contract you ship.
  • Broad suppressions: project-wide CS0618 suppression can hide accidental internal calls to the legacy API.
  • Premature removal: contract metadata is the start of a consumer migration, not proof that the migration has finished.

The strongest use of ASP.NET Core 11 OpenAPI deprecation is a single, reviewable chain from C# intent to generated contract to CI evidence. The framework supplies the mapping; your application still owns the inventory, consumer communication, compatibility window, and final removal decision.

References

Found this useful? Support more practical developer content.

Author

Practical .NET, Angular, Azure, Blazor, and AI engineering for real-world development.

Write A Comment

Ads Blocker Image Powered by Code Help Pro

Ads Blocker Detected!!!

We have detected that you are using extensions to block ads. Please support us by disabling these ads blocker.

Powered By
Best Wordpress Adblock Detecting Plugin | CHP Adblock