minimal-apis.github.io
minimal-apis.github.io copied to clipboard
Parameter optionality in Minimal APIs
What would you like see:
- [x] Documentation
- [ ] Sample
- [ ] Tutorial
Content pulled from https://blog.safia.rocks/minimal-apis-optionality.html
In .NET 6 RC1, we shipped support for a new feature in Minimal APIs that allows developers to set the optionality of request parameters by using nullable annotations and default parameters to indicate which values are required and which aren't. For example, let's say you had an endpoint that generated a random number based on a seed, like so:
var builder = WebApplication.CreateBuilder(args); var app = builder.Build(); app.MapGet("/random", (int seed, int max) => { var random = new Random(seed); return random.Next(0, max); }); app.Run();
By default, in the scenario above, the
seed
parameter will be treated as required. That means if the user sends the following request to the endpoint:$ http "http://localhost:5184/random" HTTP/1.1 400 Bad Request Content-Length: 0 Date: Sun, 22 Aug 2021 21:52:51 GMT Server: Kestrel
They'll be meet with a 400 Bad Request response. However, everything is all fine and dandy if both values are provided, though.
$ http "http://localhost:5184/random?seed=5&max=100" HTTP/1.1 200 OK Content-Type: application/json; charset=utf-8 Date: Sun, 22 Aug 2021 22:06:56 GMT Server: Kestrel Transfer-Encoding: chunked 33
As it turns out, we can generate a
Random
object without providing a seed. By annotatingseed
as a nullable property, we can permit users to send requests to the endpoint without providing aseed
property to the query.app.MapGet("/random", (int? seed, int max) => { var random = seed.HasValue ? new Random(seed.Value) : new Random(); return random.Next(0, max); });
In this case, both requests below are valid.
$ http "http://localhost:5184/random?max=100" HTTP/1.1 200 OK Content-Type: application/json; charset=utf-8 Date: Sun, 22 Aug 2021 22:08:16 GMT Server: Kestrel Transfer-Encoding: chunked 17 $ http "http://localhost:5184/random?seed=5&max=100" HTTP/1.1 200 OK Content-Type: application/json; charset=utf-8 Date: Sun, 22 Aug 2021 22:08:35 GMT Server: Kestrel Transfer-Encoding: chunked 33
However, since the
max
attribute is still required, omitting that from the request will result in the 400 Bad Request error.$ http "http://localhost:5184/random?seed=5" HTTP/1.1 400 Bad Request Content-Length: 0 Date: Sun, 22 Aug 2021 22:09:22 GMT Server: Kestrel
In addition to nullable annotations, we can indicate that a parameter is optional by specifying a default value for the parameter.
int GetRandom(int? seed, int max = 5) { var random = seed.HasValue ? new Random(seed.Value) : new Random(); return random.Next(0, max); } app.MapGet("/random", GetRandom);
:warning: Note: In the code above, the endpoint logic has been moved to a separate function since default parameters in inline lambdas are not currently supported in C#.
The change above permits the user to provide the
seed
andmax
parameters as optional within requests. All the following requests will be processed by the endpoint.$ http "http://localhost:5184/random?seed=5&max=100" HTTP/1.1 200 OK Content-Type: application/json; charset=utf-8 Date: Sun, 22 Aug 2021 22:12:58 GMT Server: Kestrel Transfer-Encoding: chunked 33 $ http "http://localhost:5184/random?seed=5" HTTP/1.1 200 OK Content-Type: application/json; charset=utf-8 Date: Sun, 22 Aug 2021 22:15:11 GMT Server: Kestrel Transfer-Encoding: chunked 1 $ http "http://localhost:5184/random" HTTP/1.1 200 OK Content-Type: application/json; charset=utf-8 Date: Sun, 22 Aug 2021 22:15:24 GMT Server: Kestrel Transfer-Encoding: chunked 4 $ http "http://localhost:5184/random?max=100" HTTP/1.1 200 OK Content-Type: application/json; charset=utf-8 Date: Sun, 22 Aug 2021 22:15:51 GMT Server: Kestrel Transfer-Encoding: chunked 95
In the above scenario, we were able to mark a query parameter as optional but the same principles apply to parameters in the body as well as services injected to the endpoint. So for example, let's say that we took the configuration for our random number generator as a set of arguments provided in the body of the request.
app.MapPost("/random", (ConfigOptions? options) => { var random = options is not null ? new Random(options.Seed) : new Random(); return random.Next(0, options?.Max ?? 100); }); app.Run(); class ConfigOptions { public int Seed { get; } public int Max { get; } }
The above will appropriately handle requests where the the body parameters are provided and those where it isn't.
One thing to note is that the behavior is different for service parameter. For one, instead of returning a 400 status code, the endpoint will return a 500 status code depending on whether or not the service was provided. Also, due to some nuances in the parameter parsing logic, optionality is only support for services that are injected into the endpoint via an explicit reference (using the
FromService
attribute) and not those that are implicit referenced.:spiral_notepad: The nuance here is that there is some subtlety around discerning whether the
SomeType
inapp.MapPost("/foo", (SomeType st) => ...)
is referring to a serviceSomeType
or a body param that deserializes toSomeType
.Finally, depending on the nullability context the parameter exists in the, the behavior of this feature will differ slightly.
- Unannotated value types are always required regardless of nullability context.
- Unannotated reference types are always optional if they exist in an unknown nullability context.
- Regardless of nullability context, you can use default values to indicate that a reference or value type parameter is optional.
One final note, by default, no message will be sent in the response when a parameter fails a requiredness check. For more information on this, including current solutions, check out this GitHub issue.