Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ jobs:
dotnet-version: |
8.0.x
9.0.x
10.0.x
- name: Restore dependencies
run: dotnet restore
- name: Build
Expand Down
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,6 @@
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageRequireLicenseAcceptance>True</PackageRequireLicenseAcceptance>

<Version>1.3.5</Version>
<Version>1.4.0</Version>
</PropertyGroup>
</Project>
19 changes: 2 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,10 +117,7 @@ internal class HelloWorldRequestValidator : AbstractValidator<HelloWorldRequest>
{
public HelloWorldRequestValidator()
{
RuleFor(x => x.Name)
.NotEmpty()
.MinimumLength(3)
.MaximumLength(50);
RuleFor(x => x.Name).NotEmpty().MinimumLength(3).MaximumLength(50);
}
}

Expand Down Expand Up @@ -151,19 +148,7 @@ internal record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary

internal class GetWeatherForecast : MinimalEndpoint<WeatherForecast[]>
{
private static readonly string[] _summaries =
[
"Freezing",
"Bracing",
"Chilly",
"Cool",
"Mild",
"Warm",
"Balmy",
"Hot",
"Sweltering",
"Scorching"
];
private static readonly string[] _summaries = ["Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"];

protected override void Configure(
EndpointConfigurationBuilder builder,
Expand Down
95 changes: 90 additions & 5 deletions docs/IAsyncEnumerableResponse.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,10 @@

`MinimalEndpointWithStreamingResponse` is a specialized base class designed to simplify the implementation of minimal APIs that return streaming responses in .NET. It provides a structured way to define endpoints that stream data asynchronously using `IAsyncEnumerable<T>`.

> **Note:** .Net 10 introduced `ServerSentEventsResult` IResult type to return IAsyncEnumerable responses which can be used by a `MinimalEndpoint`. See below for more information.

``` csharp
public record ListCustomersResponse(
Guid Id,
string FirstName,
string? MiddleName,
string LastName);
public record ListCustomersResponse(Guid Id, string FirstName, string? MiddleName, string LastName);

internal class ListCustomers(ServiceDbContext db)
: MinimalEndpointWithStreamingResponse<ListCustomersResponse>
Expand Down Expand Up @@ -58,4 +56,91 @@ internal class ListStores(ServiceDbContext db)
}
}
}
```

## For .Net 10 or Later

By utilizing `ServerSentEventResult`, it's possible to return streaming responses with `MinimalEndpoint`.
```csharp
internal class GetStreamingWeatherForecastSse
: MinimalEndpoint<IResult>
{
private static readonly string[] _summaries = ["Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"];

protected override void Configure(
EndpointConfigurationBuilder builder,
ConfigurationContext<EndpointConfigurationParameters> configurationContext)
{
builder.MapGet("/streamingweatherforecastsse")
.WithName("GetStreamingWeatherForecastSse")
.WithTags("WeatherForecastWebApi")
.Produces<SseItem<WeatherForecast>>(contentType: "text/event-stream");
}

protected override async Task<IResult> HandleAsync(CancellationToken ct)
{
await Task.CompletedTask;
return Results.ServerSentEvents<WeatherForecast>(GetForecast(ct));

async IAsyncEnumerable<WeatherForecast> GetForecast([EnumeratorCancellation] CancellationToken ct)
{
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
_summaries[Random.Shared.Next(_summaries.Length)]
))
.ToArray();

foreach (var item in forecast)
{
yield return item;
await Task.Delay(500, ct);
}
}
}
}
```

Instead of using `IResult` response type, it's also possible to use TypedResults for a more type safe approach.
```csharp
internal class GetStreamingWeatherForecastTypedSse
: MinimalEndpoint<Results<ServerSentEventsResult<WeatherForecast>, ProblemHttpResult>>
{
private static readonly string[] _summaries = ["Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"];

protected override void Configure(
EndpointConfigurationBuilder builder,
ConfigurationContext<EndpointConfigurationParameters> configurationContext)
{
builder.MapGet("/streamingweatherforecasttypedsse")
.WithName("GetStreamingWeatherForecastTypedSse")
.WithTags("WeatherForecastWebApi");
}

protected override async Task<Results<ServerSentEventsResult<WeatherForecast>, ProblemHttpResult>> HandleAsync(CancellationToken ct)
{
await Task.CompletedTask;
return TypedResults.ServerSentEvents<WeatherForecast>(GetForecast(ct));

async IAsyncEnumerable<WeatherForecast> GetForecast([EnumeratorCancellation] CancellationToken ct)
{
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
_summaries[Random.Shared.Next(_summaries.Length)]
))
.ToArray();

foreach (var item in forecast)
{
yield return item;
await Task.Delay(500, ct);
}
}
}
}
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
 k6  .\k6 run minimal_api_basic.js

/\ Grafana /‾‾/
/\ / \ |\ __ / /
/ \/ \ | |/ / / ‾‾\
/ \ | ( | (‾) |
/ __________ \ |_|\_\ \_____/

execution: local
script: minimal_api_basic.js
output: -

scenarios: (100.00%) 1 scenario, 100 max VUs, 2m20s max duration (incl. graceful stop):
* default: Up to 100 looping VUs for 1m50s over 3 stages (gracefulRampDown: 30s, gracefulStop: 30s)


✓ status was 200

checks.........................: 100.00% 8933755 out of 8933755
data_received..................: 1.5 GB 14 MB/s
data_sent......................: 911 MB 8.3 MB/s
http_req_blocked...............: avg=2.97µs min=0s med=0s max=30.58ms p(90)=0s p(95)=0s
http_req_connecting............: avg=9ns min=0s med=0s max=2.99ms p(90)=0s p(95)=0s
✓ http_req_duration..............: avg=836.33µs min=0s med=999.1µs max=49.71ms p(90)=1.67ms p(95)=2ms
{ expected_response:true }...: avg=836.33µs min=0s med=999.1µs max=49.71ms p(90)=1.67ms p(95)=2ms
http_req_failed................: 0.00% 0 out of 8933755
http_req_receiving.............: avg=29.59µs min=0s med=0s max=41.83ms p(90)=0s p(95)=0s
http_req_sending...............: avg=9.27µs min=0s med=0s max=33.06ms p(90)=0s p(95)=0s
http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s
http_req_waiting...............: avg=797.46µs min=0s med=998.8µs max=39.71ms p(90)=1.58ms p(95)=2ms
http_reqs......................: 8933755 81215.461568/s
iteration_duration.............: avg=934.51µs min=0s med=999.6µs max=72.4ms p(90)=1.99ms p(95)=2.02ms
iterations.....................: 8933755 81215.461568/s
vus............................: 1 min=1 max=100
vus_max........................: 100 min=100 max=100


running (1m50.0s), 000/100 VUs, 8933755 complete and 0 interrupted iterations
default ✓ [======================================] 000/100 VUs 1m50s


 k6  .\k6 run minimal_endpoint_basic.js

/\ Grafana /‾‾/
/\ / \ |\ __ / /
/ \/ \ | |/ / / ‾‾\
/ \ | ( | (‾) |
/ __________ \ |_|\_\ \_____/

execution: local
script: minimal_endpoint_basic.js
output: -

scenarios: (100.00%) 1 scenario, 100 max VUs, 2m20s max duration (incl. graceful stop):
* default: Up to 100 looping VUs for 1m50s over 3 stages (gracefulRampDown: 30s, gracefulStop: 30s)


✓ status was 200

checks.........................: 100.00% 8791923 out of 8791923
data_received..................: 1.5 GB 14 MB/s
data_sent......................: 941 MB 8.6 MB/s
http_req_blocked...............: avg=2.95µs min=0s med=0s max=36.05ms p(90)=0s p(95)=0s
http_req_connecting............: avg=8ns min=0s med=0s max=2.54ms p(90)=0s p(95)=0s
✓ http_req_duration..............: avg=861.35µs min=0s med=999.4µs max=42.99ms p(90)=1.69ms p(95)=2ms
{ expected_response:true }...: avg=861.35µs min=0s med=999.4µs max=42.99ms p(90)=1.69ms p(95)=2ms
http_req_failed................: 0.00% 0 out of 8791923
http_req_receiving.............: avg=30.08µs min=0s med=0s max=40.5ms p(90)=0s p(95)=0s
http_req_sending...............: avg=9.51µs min=0s med=0s max=41.34ms p(90)=0s p(95)=0s
http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s
http_req_waiting...............: avg=821.75µs min=0s med=999.1µs max=42.51ms p(90)=1.6ms p(95)=2ms
http_reqs......................: 8791923 79926.279906/s
iteration_duration.............: avg=951.89µs min=0s med=999.8µs max=58.78ms p(90)=1.99ms p(95)=2.01ms
iterations.....................: 8791923 79926.279906/s
vus............................: 1 min=1 max=100
vus_max........................: 100 min=100 max=100


running (1m50.0s), 000/100 VUs, 8791923 complete and 0 interrupted iterations
default ✓ [======================================] 000/100 VUs 1m50s


 k6  .\k6 run webresult_endpoint_basic.js

/\ Grafana /‾‾/
/\ / \ |\ __ / /
/ \/ \ | |/ / / ‾‾\
/ \ | ( | (‾) |
/ __________ \ |_|\_\ \_____/

execution: local
script: webresult_endpoint_basic.js
output: -

scenarios: (100.00%) 1 scenario, 100 max VUs, 2m20s max duration (incl. graceful stop):
* default: Up to 100 looping VUs for 1m50s over 3 stages (gracefulRampDown: 30s, gracefulStop: 30s)


✓ status was 200

checks.........................: 100.00% 8763420 out of 8763420
data_received..................: 1.5 GB 14 MB/s
data_sent......................: 955 MB 8.7 MB/s
http_req_blocked...............: avg=3µs min=0s med=0s max=31.14ms p(90)=0s p(95)=0s
http_req_connecting............: avg=11ns min=0s med=0s max=16.58ms p(90)=0s p(95)=0s
✓ http_req_duration..............: avg=867.84µs min=0s med=999.5µs max=110.19ms p(90)=1.73ms p(95)=2ms
{ expected_response:true }...: avg=867.84µs min=0s med=999.5µs max=110.19ms p(90)=1.73ms p(95)=2ms
http_req_failed................: 0.00% 0 out of 8763420
http_req_receiving.............: avg=29.66µs min=0s med=0s max=42.15ms p(90)=0s p(95)=0s
http_req_sending...............: avg=9.25µs min=0s med=0s max=52.18ms p(90)=0s p(95)=0s
http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s
http_req_waiting...............: avg=828.92µs min=0s med=999.2µs max=109.87ms p(90)=1.62ms p(95)=2ms
http_reqs......................: 8763420 79666.408091/s
iteration_duration.............: avg=958.27µs min=0s med=999.9µs max=110.7ms p(90)=1.99ms p(95)=2.01ms
iterations.....................: 8763420 79666.408091/s
vus............................: 1 min=1 max=100
vus_max........................: 100 min=100 max=100


running (1m50.0s), 000/100 VUs, 8763420 complete and 0 interrupted iterations
default ✓ [======================================] 000/100 VUs 1m50s
Loading