How to create responses in Minimal API apps
Note
This isn't the latest version of this article. For the current release, see the .NET 9 version of this article.
Warning
This version of ASP.NET Core is no longer supported. For more information, see the .NET and .NET Core Support Policy. For the current release, see the .NET 9 version of this article.
Important
This information relates to a pre-release product that may be substantially modified before it's commercially released. Microsoft makes no warranties, express or implied, with respect to the information provided here.
For the current release, see the .NET 9 version of this article.
Minimal endpoints support the following types of return values:
string
- This includesTask<string>
andValueTask<string>
.T
(Any other type) - This includesTask<T>
andValueTask<T>
.IResult
based - This includesTask<IResult>
andValueTask<IResult>
.
string
return values
Behavior | Content-Type |
---|---|
The framework writes the string directly to the response. | text/plain |
Consider the following route handler, which returns a Hello world
text.
app.MapGet("/hello", () => "Hello World");
The 200
status code is returned with text/plain
Content-Type header and the following content.
Hello World
T
(Any other type) return values
Behavior | Content-Type |
---|---|
The framework JSON-serializes the response. | application/json |
Consider the following route handler, which returns an anonymous type containing a Message
string property.
app.MapGet("/hello", () => new { Message = "Hello World" });
The 200
status code is returned with application/json
Content-Type header and the following content.
{"message":"Hello World"}
IResult
return values
Behavior | Content-Type |
---|---|
The framework calls IResult.ExecuteAsync. | Decided by the IResult implementation. |
The IResult
interface defines a contract that represents the result of an HTTP endpoint. The static Results class and the static TypedResults are used to create various IResult
objects that represent different types of responses.
TypedResults vs Results
The Results and TypedResults static classes provide similar sets of results helpers. The TypedResults
class is the typed equivalent of the Results
class. However, the Results
helpers' return type is IResult, while each TypedResults
helper's return type is one of the IResult
implementation types. The difference means that for Results
helpers a conversion is needed when the concrete type is needed, for example, for unit testing. The implementation types are defined in the Microsoft.AspNetCore.Http.HttpResults namespace.
Returning TypedResults
rather than Results
has the following advantages:
TypedResults
helpers return strongly typed objects, which can improve code readability, unit testing, and reduce the chance of runtime errors.- The implementation type automatically provides the response type metadata for OpenAPI to describe the endpoint.
Consider the following endpoint, for which a 200 OK
status code with the expected JSON response is produced.
app.MapGet("/hello", () => Results.Ok(new Message() { Text = "Hello World!" }))
.Produces<Message>();
In order to document this endpoint correctly the extensions method Produces
is called. However, it's not necessary to call Produces
if TypedResults
is used instead of Results
, as shown in the following code. TypedResults
automatically provides the metadata for the endpoint.
app.MapGet("/hello2", () => TypedResults.Ok(new Message() { Text = "Hello World!" }));
For more information about describing a response type, see OpenAPI support in minimal APIs.
As mentioned previously, when using TypedResults
, a conversion is not needed. Consider the following minimal API which returns a TypedResults
class
public static async Task<Ok<Todo[]>> GetAllTodos(TodoGroupDbContext database)
{
var todos = await database.Todos.ToArrayAsync();
return TypedResults.Ok(todos);
}
The following test checks for the full concrete type:
[Fact]
public async Task GetAllReturnsTodosFromDatabase()
{
// Arrange
await using var context = new MockDb().CreateDbContext();
context.Todos.Add(new Todo
{
Id = 1,
Title = "Test title 1",
Description = "Test description 1",
IsDone = false
});
context.Todos.Add(new Todo
{
Id = 2,
Title = "Test title 2",
Description = "Test description 2",
IsDone = true
});
await context.SaveChangesAsync();
// Act
var result = await TodoEndpointsV1.GetAllTodos(context);
//Assert
Assert.IsType<Ok<Todo[]>>(result);
Assert.NotNull(result.Value);
Assert.NotEmpty(result.Value);
Assert.Collection(result.Value, todo1 =>
{
Assert.Equal(1, todo1.Id);
Assert.Equal("Test title 1", todo1.Title);
Assert.False(todo1.IsDone);
}, todo2 =>
{
Assert.Equal(2, todo2.Id);
Assert.Equal("Test title 2", todo2.Title);
Assert.True(todo2.IsDone);
});
}
Because all methods on Results
return IResult
in their signature, the compiler automatically infers that as the request delegate return type when returning different results from a single endpoint. TypedResults
requires the use of Results<T1, TN>
from such delegates.
The following method compiles because both Results.Ok
and Results.NotFound
are declared as returning IResult
, even though the actual concrete types of the objects returned are different:
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());
The following method does not compile, because TypedResults.Ok
and TypedResults.NotFound
are declared as returning different types and the compiler won't attempt to infer the best matching type:
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(todo)
: TypedResults.NotFound());
To use TypedResults
, the return type must be fully declared, which when asynchronous requires the Task<>
wrapper. Using TypedResults
is more verbose, but that's the trade-off for having the type information be statically available and thus capable of self-describing to OpenAPI:
app.MapGet("/todoitems/{id}", async Task<Results<Ok<Todo>, NotFound>> (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(todo)
: TypedResults.NotFound());
Results<TResult1, TResultN>
Use Results<TResult1, TResultN>
as the endpoint handler return type instead of IResult
when:
- Multiple
IResult
implementation types are returned from the endpoint handler. - The static
TypedResult
class is used to create theIResult
objects.
This alternative is better than returning IResult
because the generic union types automatically retain the endpoint metadata. And since the Results<TResult1, TResultN>
union types implement implicit cast operators, the compiler can automatically convert the types specified in the generic arguments to an instance of the union type.
This has the added benefit of providing compile-time checking that a route handler actually only returns the results that it declares it does. Attempting to return a type that isn't declared as one of the generic arguments to Results<>
results in a compilation error.
Consider the following endpoint, for which a 400 BadRequest
status code is returned when the orderId
is greater than 999
. Otherwise, it produces a 200 OK
with the expected content.
app.MapGet("/orders/{orderId}", IResult (int orderId)
=> orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)))
.Produces(400)
.Produces<Order>();
In order to document this endpoint correctly the extension method Produces
is called. However, since the TypedResults
helper automatically includes the metadata for the endpoint, you can return the Results<T1, Tn>
union type instead, as shown in the following code.
app.MapGet("/orders/{orderId}", Results<BadRequest, Ok<Order>> (int orderId)
=> orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)));
Built-in results
Common result helpers exist in the Results and TypedResults static classes. Returning TypedResults
is preferred to returning Results
. For more information, see TypedResults vs Results.
The following sections demonstrate the usage of the common result helpers.
JSON
app.MapGet("/hello", () => Results.Json(new { Message = "Hello World" }));
WriteAsJsonAsync is an alternative way to return JSON:
app.MapGet("/", (HttpContext context) => context.Response.WriteAsJsonAsync
(new { Message = "Hello World" }));
Custom Status Code
app.MapGet("/405", () => Results.StatusCode(405));
Internal Server Error
app.MapGet("/500", () => Results.InternalServerError("Something went wrong!"));
The preceding example returns a 500 status code.
Text
app.MapGet("/text", () => Results.Text("This is some text"));
Stream
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
var proxyClient = new HttpClient();
app.MapGet("/pokemon", async () =>
{
var stream = await proxyClient.GetStreamAsync("http://contoso/pokedex.json");
// Proxy the response as JSON
return Results.Stream(stream, "application/json");
});
app.Run();
Results.Stream
overloads allow access to the underlying HTTP response stream without buffering. The following example uses ImageSharp to return a reduced size of the specified image:
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Processing;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/process-image/{strImage}", (string strImage, HttpContext http, CancellationToken token) =>
{
http.Response.Headers.CacheControl = $"public,max-age={TimeSpan.FromHours(24).TotalSeconds}";
return Results.Stream(stream => ResizeImageAsync(strImage, stream, token), "image/jpeg");
});
async Task ResizeImageAsync(string strImage, Stream stream, CancellationToken token)
{
var strPath = $"wwwroot/img/{strImage}";
using var image = await Image.LoadAsync(strPath, token);
int width = image.Width / 2;
int height = image.Height / 2;
image.Mutate(x =>x.Resize(width, height));
await image.SaveAsync(stream, JpegFormat.Instance, cancellationToken: token);
}
The following example streams an image from Azure Blob storage:
app.MapGet("/stream-image/{containerName}/{blobName}",
async (string blobName, string containerName, CancellationToken token) =>
{
var conStr = builder.Configuration["blogConStr"];
BlobContainerClient blobContainerClient = new BlobContainerClient(conStr, containerName);
BlobClient blobClient = blobContainerClient.GetBlobClient(blobName);
return Results.Stream(await blobClient.OpenReadAsync(cancellationToken: token), "image/jpeg");
});
The following example streams a video from an Azure Blob:
// GET /stream-video/videos/earth.mp4
app.MapGet("/stream-video/{containerName}/{blobName}",
async (HttpContext http, CancellationToken token, string blobName, string containerName) =>
{
var conStr = builder.Configuration["blogConStr"];
BlobContainerClient blobContainerClient = new BlobContainerClient(conStr, containerName);
BlobClient blobClient = blobContainerClient.GetBlobClient(blobName);
var properties = await blobClient.GetPropertiesAsync(cancellationToken: token);
DateTimeOffset lastModified = properties.Value.LastModified;
long length = properties.Value.ContentLength;
long etagHash = lastModified.ToFileTime() ^ length;
var entityTag = new EntityTagHeaderValue('\"' + Convert.ToString(etagHash, 16) + '\"');
http.Response.Headers.CacheControl = $"public,max-age={TimeSpan.FromHours(24).TotalSeconds}";
return Results.Stream(await blobClient.OpenReadAsync(cancellationToken: token),
contentType: "video/mp4",
lastModified: lastModified,
entityTag: entityTag,
enableRangeProcessing: true);
});
Redirect
app.MapGet("/old-path", () => Results.Redirect("/new-path"));
File
app.MapGet("/download", () => Results.File("myfile.text"));
HttpResult interfaces
The following interfaces in the Microsoft.AspNetCore.Http namespace provide a way to detect the IResult
type at runtime, which is a common pattern in filter implementations:
- IContentTypeHttpResult
- IFileHttpResult
- INestedHttpResult
- IStatusCodeHttpResult
- IValueHttpResult
- IValueHttpResult<TValue>
Here's an example of a filter that uses one of these interfaces:
app.MapGet("/weatherforecast", (int days) =>
{
if (days <= 0)
{
return Results.BadRequest();
}
var forecast = Enumerable.Range(1, days).Select(index =>
new WeatherForecast(DateTime.Now.AddDays(index), Random.Shared.Next(-20, 55), "Cool"))
.ToArray();
return Results.Ok(forecast);
}).
AddEndpointFilter(async (context, next) =>
{
var result = await next(context);
return result switch
{
IValueHttpResult<WeatherForecast[]> weatherForecastResult => new WeatherHttpResult(weatherForecastResult.Value),
_ => result
};
});
For more information, see Filters in Minimal API apps and IResult implementation types.
Customizing responses
Applications can control responses by implementing a custom IResult type. The following code is an example of an HTML result type:
using System.Net.Mime;
using System.Text;
static class ResultsExtensions
{
public static IResult Html(this IResultExtensions resultExtensions, string html)
{
ArgumentNullException.ThrowIfNull(resultExtensions);
return new HtmlResult(html);
}
}
class HtmlResult : IResult
{
private readonly string _html;
public HtmlResult(string html)
{
_html = html;
}
public Task ExecuteAsync(HttpContext httpContext)
{
httpContext.Response.ContentType = MediaTypeNames.Text.Html;
httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
return httpContext.Response.WriteAsync(_html);
}
}
We recommend adding an extension method to Microsoft.AspNetCore.Http.IResultExtensions to make these custom results more discoverable.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/html", () => Results.Extensions.Html(@$"<!doctype html>
<html>
<head><title>miniHTML</title></head>
<body>
<h1>Hello World</h1>
<p>The time on the server is {DateTime.Now:O}</p>
</body>
</html>"));
app.Run();
Also, a custom IResult
type can provide its own annotation by implementing the IEndpointMetadataProvider interface. For example, the following code adds an annotation to the preceding HtmlResult
type that describes the response produced by the endpoint.
class HtmlResult : IResult, IEndpointMetadataProvider
{
private readonly string _html;
public HtmlResult(string html)
{
_html = html;
}
public Task ExecuteAsync(HttpContext httpContext)
{
httpContext.Response.ContentType = MediaTypeNames.Text.Html;
httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
return httpContext.Response.WriteAsync(_html);
}
public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
{
builder.Metadata.Add(new ProducesHtmlMetadata());
}
}
The ProducesHtmlMetadata
is an implementation of IProducesResponseTypeMetadata that defines the produced response content type text/html
and the status code 200 OK
.
internal sealed class ProducesHtmlMetadata : IProducesResponseTypeMetadata
{
public Type? Type => null;
public int StatusCode => 200;
public IEnumerable<string> ContentTypes { get; } = new[] { MediaTypeNames.Text.Html };
}
An alternative approach is using the Microsoft.AspNetCore.Mvc.ProducesAttribute to describe the produced response. The following code changes the PopulateMetadata
method to use ProducesAttribute
.
public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
{
builder.Metadata.Add(new ProducesAttribute(MediaTypeNames.Text.Html));
}
Configure JSON serialization options
By default, minimal API apps use Web defaults
options during JSON serialization and deserialization.
Configure JSON serialization options globally
Options can be configured globally for an app by invoking ConfigureHttpJsonOptions. The following example includes public fields and formats JSON output.
var builder = WebApplication.CreateBuilder(args);
builder.Services.ConfigureHttpJsonOptions(options => {
options.SerializerOptions.WriteIndented = true;
options.SerializerOptions.IncludeFields = true;
});
var app = builder.Build();
app.MapPost("/", (Todo todo) => {
if (todo is not null) {
todo.Name = todo.NameField;
}
return todo;
});
app.Run();
class Todo {
public string? Name { get; set; }
public string? NameField;
public bool IsComplete { get; set; }
}
// If the request body contains the following JSON:
//
// {"nameField":"Walk dog", "isComplete":false}
//
// The endpoint returns the following JSON:
//
// {
// "name":"Walk dog",
// "nameField":"Walk dog",
// "isComplete":false
// }
Since fields are included, the preceding code reads NameField
and includes it in the output JSON.
Configure JSON serialization options for an endpoint
To configure serialization options for an endpoint, invoke Results.Json and pass to it a JsonSerializerOptions object, as shown in the following example:
using System.Text.Json;
var app = WebApplication.Create();
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web)
{ WriteIndented = true };
app.MapGet("/", () =>
Results.Json(new Todo { Name = "Walk dog", IsComplete = false }, options));
app.Run();
class Todo
{
public string? Name { get; set; }
public bool IsComplete { get; set; }
}
// The endpoint returns the following JSON:
//
// {
// "name":"Walk dog",
// "isComplete":false
// }
As an alternative, use an overload of WriteAsJsonAsync that accepts a JsonSerializerOptions object. The following example uses this overload to format the output JSON:
using System.Text.Json;
var app = WebApplication.Create();
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web) {
WriteIndented = true };
app.MapGet("/", (HttpContext context) =>
context.Response.WriteAsJsonAsync<Todo>(
new Todo { Name = "Walk dog", IsComplete = false }, options));
app.Run();
class Todo
{
public string? Name { get; set; }
public bool IsComplete { get; set; }
}
// The endpoint returns the following JSON:
//
// {
// "name":"Walk dog",
// "isComplete":false
// }
Additional Resources
Minimal endpoints support the following types of return values:
string
- This includesTask<string>
andValueTask<string>
.T
(Any other type) - This includesTask<T>
andValueTask<T>
.IResult
based - This includesTask<IResult>
andValueTask<IResult>
.
string
return values
Behavior | Content-Type |
---|---|
The framework writes the string directly to the response. | text/plain |
Consider the following route handler, which returns a Hello world
text.
app.MapGet("/hello", () => "Hello World");
The 200
status code is returned with text/plain
Content-Type header and the following content.
Hello World
T
(Any other type) return values
Behavior | Content-Type |
---|---|
The framework JSON-serializes the response. | application/json |
Consider the following route handler, which returns an anonymous type containing a Message
string property.
app.MapGet("/hello", () => new { Message = "Hello World" });
The 200
status code is returned with application/json
Content-Type header and the following content.
{"message":"Hello World"}
IResult
return values
Behavior | Content-Type |
---|---|
The framework calls IResult.ExecuteAsync. | Decided by the IResult implementation. |
The IResult
interface defines a contract that represents the result of an HTTP endpoint. The static Results class and the static TypedResults are used to create various IResult
objects that represent different types of responses.
TypedResults vs Results
The Results and TypedResults static classes provide similar sets of results helpers. The TypedResults
class is the typed equivalent of the Results
class. However, the Results
helpers' return type is IResult, while each TypedResults
helper's return type is one of the IResult
implementation types. The difference means that for Results
helpers a conversion is needed when the concrete type is needed, for example, for unit testing. The implementation types are defined in the Microsoft.AspNetCore.Http.HttpResults namespace.
Returning TypedResults
rather than Results
has the following advantages:
TypedResults
helpers return strongly typed objects, which can improve code readability, unit testing, and reduce the chance of runtime errors.- The implementation type automatically provides the response type metadata for OpenAPI to describe the endpoint.
Consider the following endpoint, for which a 200 OK
status code with the expected JSON response is produced.
app.MapGet("/hello", () => Results.Ok(new Message() { Text = "Hello World!" }))
.Produces<Message>();
In order to document this endpoint correctly the extensions method Produces
is called. However, it's not necessary to call Produces
if TypedResults
is used instead of Results
, as shown in the following code. TypedResults
automatically provides the metadata for the endpoint.
app.MapGet("/hello2", () => TypedResults.Ok(new Message() { Text = "Hello World!" }));
For more information about describing a response type, see OpenAPI support in minimal APIs.
As mentioned previously, when using TypedResults
, a conversion is not needed. Consider the following minimal API which returns a TypedResults
class
public static async Task<Ok<Todo[]>> GetAllTodos(TodoGroupDbContext database)
{
var todos = await database.Todos.ToArrayAsync();
return TypedResults.Ok(todos);
}
The following test checks for the full concrete type:
[Fact]
public async Task GetAllReturnsTodosFromDatabase()
{
// Arrange
await using var context = new MockDb().CreateDbContext();
context.Todos.Add(new Todo
{
Id = 1,
Title = "Test title 1",
Description = "Test description 1",
IsDone = false
});
context.Todos.Add(new Todo
{
Id = 2,
Title = "Test title 2",
Description = "Test description 2",
IsDone = true
});
await context.SaveChangesAsync();
// Act
var result = await TodoEndpointsV1.GetAllTodos(context);
//Assert
Assert.IsType<Ok<Todo[]>>(result);
Assert.NotNull(result.Value);
Assert.NotEmpty(result.Value);
Assert.Collection(result.Value, todo1 =>
{
Assert.Equal(1, todo1.Id);
Assert.Equal("Test title 1", todo1.Title);
Assert.False(todo1.IsDone);
}, todo2 =>
{
Assert.Equal(2, todo2.Id);
Assert.Equal("Test title 2", todo2.Title);
Assert.True(todo2.IsDone);
});
}
Because all methods on Results
return IResult
in their signature, the compiler automatically infers that as the request delegate return type when returning different results from a single endpoint. TypedResults
requires the use of Results<T1, TN>
from such delegates.
The following method compiles because both Results.Ok
and Results.NotFound
are declared as returning IResult
, even though the actual concrete types of the objects returned are different:
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());
The following method does not compile, because TypedResults.Ok
and TypedResults.NotFound
are declared as returning different types and the compiler won't attempt to infer the best matching type:
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(todo)
: TypedResults.NotFound());
To use TypedResults
, the return type must be fully declared, which when asynchronous requires the Task<>
wrapper. Using TypedResults
is more verbose, but that's the trade-off for having the type information be statically available and thus capable of self-describing to OpenAPI:
app.MapGet("/todoitems/{id}", async Task<Results<Ok<Todo>, NotFound>> (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(todo)
: TypedResults.NotFound());
Results<TResult1, TResultN>
Use Results<TResult1, TResultN>
as the endpoint handler return type instead of IResult
when:
- Multiple
IResult
implementation types are returned from the endpoint handler. - The static
TypedResult
class is used to create theIResult
objects.
This alternative is better than returning IResult
because the generic union types automatically retain the endpoint metadata. And since the Results<TResult1, TResultN>
union types implement implicit cast operators, the compiler can automatically convert the types specified in the generic arguments to an instance of the union type.
This has the added benefit of providing compile-time checking that a route handler actually only returns the results that it declares it does. Attempting to return a type that isn't declared as one of the generic arguments to Results<>
results in a compilation error.
Consider the following endpoint, for which a 400 BadRequest
status code is returned when the orderId
is greater than 999
. Otherwise, it produces a 200 OK
with the expected content.
app.MapGet("/orders/{orderId}", IResult (int orderId)
=> orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)))
.Produces(400)
.Produces<Order>();
In order to document this endpoint correctly the extension method Produces
is called. However, since the TypedResults
helper automatically includes the metadata for the endpoint, you can return the Results<T1, Tn>
union type instead, as shown in the following code.
app.MapGet("/orders/{orderId}", Results<BadRequest, Ok<Order>> (int orderId)
=> orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)));
Built-in results
Common result helpers exist in the Results and TypedResults static classes. Returning TypedResults
is preferred to returning Results
. For more information, see TypedResults vs Results.
The following sections demonstrate the usage of the common result helpers.
JSON
app.MapGet("/hello", () => Results.Json(new { Message = "Hello World" }));
WriteAsJsonAsync is an alternative way to return JSON:
app.MapGet("/", (HttpContext context) => context.Response.WriteAsJsonAsync
(new { Message = "Hello World" }));
Custom Status Code
app.MapGet("/405", () => Results.StatusCode(405));
Text
app.MapGet("/text", () => Results.Text("This is some text"));
Stream
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
var proxyClient = new HttpClient();
app.MapGet("/pokemon", async () =>
{
var stream = await proxyClient.GetStreamAsync("http://contoso/pokedex.json");
// Proxy the response as JSON
return Results.Stream(stream, "application/json");
});
app.Run();
Results.Stream
overloads allow access to the underlying HTTP response stream without buffering. The following example uses ImageSharp to return a reduced size of the specified image:
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Processing;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/process-image/{strImage}", (string strImage, HttpContext http, CancellationToken token) =>
{
http.Response.Headers.CacheControl = $"public,max-age={TimeSpan.FromHours(24).TotalSeconds}";
return Results.Stream(stream => ResizeImageAsync(strImage, stream, token), "image/jpeg");
});
async Task ResizeImageAsync(string strImage, Stream stream, CancellationToken token)
{
var strPath = $"wwwroot/img/{strImage}";
using var image = await Image.LoadAsync(strPath, token);
int width = image.Width / 2;
int height = image.Height / 2;
image.Mutate(x =>x.Resize(width, height));
await image.SaveAsync(stream, JpegFormat.Instance, cancellationToken: token);
}
The following example streams an image from Azure Blob storage:
app.MapGet("/stream-image/{containerName}/{blobName}",
async (string blobName, string containerName, CancellationToken token) =>
{
var conStr = builder.Configuration["blogConStr"];
BlobContainerClient blobContainerClient = new BlobContainerClient(conStr, containerName);
BlobClient blobClient = blobContainerClient.GetBlobClient(blobName);
return Results.Stream(await blobClient.OpenReadAsync(cancellationToken: token), "image/jpeg");
});
The following example streams a video from an Azure Blob:
// GET /stream-video/videos/earth.mp4
app.MapGet("/stream-video/{containerName}/{blobName}",
async (HttpContext http, CancellationToken token, string blobName, string containerName) =>
{
var conStr = builder.Configuration["blogConStr"];
BlobContainerClient blobContainerClient = new BlobContainerClient(conStr, containerName);
BlobClient blobClient = blobContainerClient.GetBlobClient(blobName);
var properties = await blobClient.GetPropertiesAsync(cancellationToken: token);
DateTimeOffset lastModified = properties.Value.LastModified;
long length = properties.Value.ContentLength;
long etagHash = lastModified.ToFileTime() ^ length;
var entityTag = new EntityTagHeaderValue('\"' + Convert.ToString(etagHash, 16) + '\"');
http.Response.Headers.CacheControl = $"public,max-age={TimeSpan.FromHours(24).TotalSeconds}";
return Results.Stream(await blobClient.OpenReadAsync(cancellationToken: token),
contentType: "video/mp4",
lastModified: lastModified,
entityTag: entityTag,
enableRangeProcessing: true);
});
Redirect
app.MapGet("/old-path", () => Results.Redirect("/new-path"));
File
app.MapGet("/download", () => Results.File("myfile.text"));
HttpResult interfaces
The following interfaces in the Microsoft.AspNetCore.Http namespace provide a way to detect the IResult
type at runtime, which is a common pattern in filter implementations:
- IContentTypeHttpResult
- IFileHttpResult
- INestedHttpResult
- IStatusCodeHttpResult
- IValueHttpResult
- IValueHttpResult<TValue>
Here's an example of a filter that uses one of these interfaces:
app.MapGet("/weatherforecast", (int days) =>
{
if (days <= 0)
{
return Results.BadRequest();
}
var forecast = Enumerable.Range(1, days).Select(index =>
new WeatherForecast(DateTime.Now.AddDays(index), Random.Shared.Next(-20, 55), "Cool"))
.ToArray();
return Results.Ok(forecast);
}).
AddEndpointFilter(async (context, next) =>
{
var result = await next(context);
return result switch
{
IValueHttpResult<WeatherForecast[]> weatherForecastResult => new WeatherHttpResult(weatherForecastResult.Value),
_ => result
};
});
For more information, see Filters in Minimal API apps and IResult implementation types.
Customizing responses
Applications can control responses by implementing a custom IResult type. The following code is an example of an HTML result type:
using System.Net.Mime;
using System.Text;
static class ResultsExtensions
{
public static IResult Html(this IResultExtensions resultExtensions, string html)
{
ArgumentNullException.ThrowIfNull(resultExtensions);
return new HtmlResult(html);
}
}
class HtmlResult : IResult
{
private readonly string _html;
public HtmlResult(string html)
{
_html = html;
}
public Task ExecuteAsync(HttpContext httpContext)
{
httpContext.Response.ContentType = MediaTypeNames.Text.Html;
httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
return httpContext.Response.WriteAsync(_html);
}
}
We recommend adding an extension method to Microsoft.AspNetCore.Http.IResultExtensions to make these custom results more discoverable.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/html", () => Results.Extensions.Html(@$"<!doctype html>
<html>
<head><title>miniHTML</title></head>
<body>
<h1>Hello World</h1>
<p>The time on the server is {DateTime.Now:O}</p>
</body>
</html>"));
app.Run();
Also, a custom IResult
type can provide its own annotation by implementing the IEndpointMetadataProvider interface. For example, the following code adds an annotation to the preceding HtmlResult
type that describes the response produced by the endpoint.
class HtmlResult : IResult, IEndpointMetadataProvider
{
private readonly string _html;
public HtmlResult(string html)
{
_html = html;
}
public Task ExecuteAsync(HttpContext httpContext)
{
httpContext.Response.ContentType = MediaTypeNames.Text.Html;
httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
return httpContext.Response.WriteAsync(_html);
}
public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
{
builder.Metadata.Add(new ProducesHtmlMetadata());
}
}
The ProducesHtmlMetadata
is an implementation of IProducesResponseTypeMetadata that defines the produced response content type text/html
and the status code 200 OK
.
internal sealed class ProducesHtmlMetadata : IProducesResponseTypeMetadata
{
public Type? Type => null;
public int StatusCode => 200;
public IEnumerable<string> ContentTypes { get; } = new[] { MediaTypeNames.Text.Html };
}
An alternative approach is using the Microsoft.AspNetCore.Mvc.ProducesAttribute to describe the produced response. The following code changes the PopulateMetadata
method to use ProducesAttribute
.
public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
{
builder.Metadata.Add(new ProducesAttribute(MediaTypeNames.Text.Html));
}
Configure JSON serialization options
By default, minimal API apps use Web defaults
options during JSON serialization and deserialization.
Configure JSON serialization options globally
Options can be configured globally for an app by invoking ConfigureHttpJsonOptions. The following example includes public fields and formats JSON output.
var builder = WebApplication.CreateBuilder(args);
builder.Services.ConfigureHttpJsonOptions(options => {
options.SerializerOptions.WriteIndented = true;
options.SerializerOptions.IncludeFields = true;
});
var app = builder.Build();
app.MapPost("/", (Todo todo) => {
if (todo is not null) {
todo.Name = todo.NameField;
}
return todo;
});
app.Run();
class Todo {
public string? Name { get; set; }
public string? NameField;
public bool IsComplete { get; set; }
}
// If the request body contains the following JSON:
//
// {"nameField":"Walk dog", "isComplete":false}
//
// The endpoint returns the following JSON:
//
// {
// "name":"Walk dog",
// "nameField":"Walk dog",
// "isComplete":false
// }
Since fields are included, the preceding code reads NameField
and includes it in the output JSON.
Configure JSON serialization options for an endpoint
To configure serialization options for an endpoint, invoke Results.Json and pass to it a JsonSerializerOptions object, as shown in the following example:
using System.Text.Json;
var app = WebApplication.Create();
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web)
{ WriteIndented = true };
app.MapGet("/", () =>
Results.Json(new Todo { Name = "Walk dog", IsComplete = false }, options));
app.Run();
class Todo
{
public string? Name { get; set; }
public bool IsComplete { get; set; }
}
// The endpoint returns the following JSON:
//
// {
// "name":"Walk dog",
// "isComplete":false
// }
As an alternative, use an overload of WriteAsJsonAsync that accepts a JsonSerializerOptions object. The following example uses this overload to format the output JSON:
using System.Text.Json;
var app = WebApplication.Create();
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web) {
WriteIndented = true };
app.MapGet("/", (HttpContext context) =>
context.Response.WriteAsJsonAsync<Todo>(
new Todo { Name = "Walk dog", IsComplete = false }, options));
app.Run();
class Todo
{
public string? Name { get; set; }
public bool IsComplete { get; set; }
}
// The endpoint returns the following JSON:
//
// {
// "name":"Walk dog",
// "isComplete":false
// }