In ASP.NET Core, the Minimal API framework provides a (bit more) lightweight model for building HTTP APIs. The overall structure of an endpoint is largely comparable to a controller method and differences are only in the underlying middleware processing and simplified ceremony of involved types and stages.
A Minimal API endpoint is expected to return a IResult (or ITypedResult) type. Both can be used interchangeably, but each exist to serve in a specific scenario.
Inferring Return Types
IResult is the fundamental abstraction for Minimal API responses and represents the base of anything that can be written to an HTTP response. Its programming interface closely resembles the MVC’s IActionResult type centered on an executor method.
public interface IResult{ Task ExecuteAsync(HttpContext httpContext);}
You use IResult when you need to stream content, manage custom headers, return canonical responses (e.g., JSON), non-standard content (PDF, Server-Sent events), or multipart responses.
ITypedResult is a marker interface used by helpers such as Results.Ok<T> and Results.Json<T>. It wraps an IResult with type information for metadata purposes, mainly to improve the output of tools such as OpenAPI / Swagger and support static analysis tools. As a developer, you use ITypedResult if API consumers need to know the business/domain type returned on success. Otherwise, IResult is simpler.
Results in Action
Even though IResult and ITypedResult live side by side, in real code you never use other than static methods on the Results type. Here’s a very common piece of code.
app.MapGet("/user/{id}", (int id) =>{ var user = GetUser(id); if (user == null) return Results.NotFound(); return Results.Ok(user);});
Concretely, the variable user in the code may be a concrete User type, a nullable User? type, an untyped object instance or a even dynamic type. When a strongly typed object is being returned, Results.Ok infers the type and gets its metadata. If the value is known as object or dynamic, then weak metadata is returned and Swagger sees a generic object type . Using the generic overload Results.Ok<T> helps performing a logical “cast” of the type being returned for the purpose of letting more metadata information flow to the clients. However, for this specific purpose, another option is available.
The Produces<T> Option
Minimal APIs try to infer response types from Results.Ok(user) and, as mentioned Results.Ok<T> can be used to add details. Unfortunately, this pattern works well only in simple situations. In complex endpoints, such as when the method has multiple return paths, is streaming data, returns untyped variables, or custom IResult, inference can fail and subsequently Swagger loses schema info. In this case, Produces<T>() comes to the rescue and helps restoring metadata:
app.MapGet("/user/{id}", (int id) =>{ if (id == 0) return Results.NotFound(); return Results.Ok(GetUser(id));}).Produces<User>(200);
In a nutshell, the rule of thumb is using Produces<T>() when inference fails or for public/complex APIs. Otherwise, relying on Results.Ok is sufficient.
Swagger and Endpoint Metadata
Whether metadata is inferred (using Results) or declared explicitly (using Produces), the bytes sent to the client are always the same. In other words, the HTTP payload does not change at all but only the metadata as consumed by OpenAPI/Swagger changes.
Sounds confusing? Well, to some extent, it is. Let’s clarify.
Minimal API is based on two independent pipelines. One serves the runtime execution of the code behind the endpoint and controls what the client actually receives. The other is the metadata pipeline and controls what tools think the endpoint returns. The output of the metadata pipeline has impact on the reported OpenAPI schema, the Swagger UI and anything like client code and documentation that may be generated dynamically.
As mentioned, though, the two pipelines do not influence each other. Which leads to the crucial point: if the HTTP payload doesn’t change whether Produces is used or not, how is the use of Produces<T> impacting the behavior of Swagger and similar tools?
Swagger and other tools do not consume Minimal API endpoints to figure out their behavior (status codes, headers, payloads); they just read specific endpoints that just contain endpoint metadata. These metadata endpoint expose the content that the same Minimal API infrastructure creates when it comes to processing the declared endpoints listed in the application’s startup. As the system goes through the list of MapGet calls, it populates a dictionary called endpoint data source, where it saves information inferred or declared via Produces<T>. In this context, what declared via Produces<T> wins over type inference.
Is it then possible for a developer to mimic the behavior of Swagger and inspect programmatically the metadata of a Minimal API? You bet!
Mimicking Swagger
In ASP.NET Core, every route (MapGet, MapPost, controllers) is compiled into an container stored in an endpoint-specific data source. Each endpoint contains metadata including route pattern, HTTP methods, response types, content types, authorization info, and more.
Swagger does not parse code — it reads all the metadata at runtime to generate OpenAPI. How does that? Here’s a snippet of code conceptually equivalent to the code Swagger uses to inspect Minimal API metadata programmatically:
app.MapGet("/debug/endpoints", (IEnumerable<EndpointDataSource> sources) =>{ return sources .SelectMany(s => s.Endpoints) .Select(e => new { Route = (e as RouteEndpoint)?.RoutePattern.RawText, Methods = e.Metadata.OfType<HttpMethodMetadata>().FirstOrDefault()?.HttpMethods, Responses = e.Metadata.OfType<ProducesResponseTypeMetadata>() .Select(r => new { r.StatusCode, Type = r.Type?.Name }) });});
This code returns a JSON list of routes, HTTP methods, and declared response types — effectively showing the same metadata Swagger uses.
Summary
When writing Minimal APIs, you commonly return values through one of the static methods on the Results type. Even though the documentation mentions IResult and ITypedResult interfaces under the hood of the Results type, their internal differences very rarely surface to the level of developers. Type inference is the magic that ensures metadata is exposed in a transparent way to Swagger and similar clients. When inference may not be enough, you resort to Produces<T>.