While manually defining response types offers a lot of control, it can also be very repetitive, especially for large APIs. To simplify this process, .NET provides us with a feature called TypedResults
. This allows us to automatically define response types based on the method’s return type.
The TypedResults
class is the typed equivalent of the Results
class. But there are still differences between the two. The methods of the Results
class have a return type of IResult
. On the other hand, the TypedResults
class’ methods return one of the IResult
implementation types. This means that for the Results
class, a conversion is needed when the concrete type is needed, for example, for unit testing.
Another important thing to note is that when using TypedResults
, we get an automatic description of what our endpoints return when it comes to status codes.
To see this in practice, let’s open the SubtaskEndpoints
class:
routeBuilder.MapGet("api/projects/{projectId:guid}/subtasks", async ( [FromRoute] Guid projectId, [FromServices] IServiceManager serviceManager, CancellationToken cancellationToken) => { var subtasks = await serviceManager.SubtaskService .GetAllSubtaskForProjectAsync( projectId, cancellationToken); return TypedResults.Ok(subtasks); });
We start by updating the GET api/projects/{projectId:guid}/subtasks
endpoint. The only change we have to make is to replace the Results
class with the TypedResults
one. This will produce the same result as if we used the Produces<IEnumerable<SubtaskResponse>>()
method.
Let’s go to Swagger and check how the endpoint is documented:
Here, we still get the 200 OK
status code, but we also have the JSON
representation of what the endpoint returns.
Now, let’s move to a more complex endpoint in the SubtaskEndpoints
class:
routeBuilder.MapPatch("api/projects/{projectId:guid}/subtasks/{id:guid}", async Task<Results<NoContent, NotFound, BadRequest<string>, UnprocessableEntity<IDictionary<string, string[]>>>> ( [FromRoute] Guid projectId, [FromRoute] Guid id, [FromBody] JsonElement jsonElement, [FromServices] IServiceManager serviceManager, [FromServices] IValidator<UpdateSubtaskRequest> validator, CancellationToken cancellationToken) => { var patchDocument = JsonConvert .DeserializeObject<JsonPatchDocument<UpdateSubtaskRequest>>( jsonElement.GetRawText()); if (patchDocument is null) { return TypedResults.BadRequest("Patch document object is null."); } var (subtask, updateRequest) = await serviceManager.SubtaskService .GetSubtaskForPatchingAsync(projectId, id, trackChanges: true, cancellationToken); var errors = new List<string>(); patchDocument.ApplyTo(updateRequest, (error) => { errors.Add(error.ErrorMessage); }); if (errors.Any()) { IDictionary<string, string[]> errorsDict = new Dictionary<string, string[]> { { "model-binding errors", errors.ToArray() } }; return TypedResults.UnprocessableEntity(errorsDict); } var validationResult = await validator.ValidateAsync(updateRequest, cancellationToken); if (!validationResult.IsValid) { return TypedResults.UnprocessableEntity(validationResult.ToDictionary()); } await serviceManager.SubtaskService .PatchSubtaskAsync(subtask, updateRequest, cancellationToken); return TypedResults.NoContent(); });
The first thing we do in the PATCH api/projects/{projectId:guid}/subtasks/{id:guid}
endpoint is to update the return type of our endpoint. We use a Task
of the Results<TResult1, TResultN>
type. This is required because we use the TypedResult
type and our endpoint can return multiple status codes.
It’s also vital to get the proper endpoint metadata. Here, we will return NoContent
, NotFound
, BadRequest<string>
, or UnprocessableEntity<IDictionary<string, string[]>>
.
Next, we have to modify the line where we use the BadRequest()
method. The only change here is to use the TypedResults
class instead of the Results
class.
Then, if we have patch document errors, we use the UnprocessableEntity()
method and pass the errorDict
variable as an argument. Note that we also use an explicit type of IDictionary<string, string[]>
instead of var when initializing the variable. This is because we stated that we’ll return IDictionary<string, string[]>
with the 422 Unprocessable Entity
status code.
There is also the ValidationProblem()
method of the TypedResults
class but its status code is set to 400 Bad Request
and we cannot change it. This is why we opt for the UnprocessableEntity()
method.
Next, we have another call to the UnprocessableEntity()
method.
Finally, we replace the Results
class with the TypedResults
class to call the NoContent()
method.
With all these changes in place, let’s check the UI and see what we have:
We have the four expected status codes, but there is a problem. The media type is not application/problem+json
but just application/json
. The returned object is also not quite correct.
If we use the ValidationProblem()
method instead of the UnprocessableEntity()
one, we’ll get the correct media type and object but we will not have the 422 Unprocessable Content
status code. The TypedResults
class is a new addition to .NET and there are still some quirks to sort out.
For this reason, we’ll stick to manually documenting the endpoints in our project. But at least, you are aware of the second approach as well. Great.
We’ve further improved our API documentation by defining the endpoints’ response types.
In the next module, we’ll explore filters in minimal API and how to utilize them.