Home > Back-end >  ASP.NET Core 6.0 - Minimal APIs: What is the proper way to bind json body request to desired class?
ASP.NET Core 6.0 - Minimal APIs: What is the proper way to bind json body request to desired class?

Time:01-03

I'm developing a very simple REST API using ASP.NET Core 6.0 - Minimal APIs and for one of Post methods, I need to validate the json body of the request. I used System.ComponentModel.DataAnnotations for that purpose and the code is working fine:

using System.ComponentModel.DataAnnotations;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapPost("/API_v1/Send", (PostRequest request) =>
{
    ICollection<ValidationResult> ValidationResults = null;
    if (Validate(request, out ValidationResults))
    {
        //request object is valid and has proper values
        //the rest of the logic...
    }
    return new { status = "failed"};
});

app.Run();

static bool Validate<T>(T obj, out ICollection<ValidationResult> results)
{
    results = new List<ValidationResult>();
    return Validator.TryValidateObject(obj, new ValidationContext(obj), results, true);
}


public class PostRequest
{
    [Required]
    [MinLength(1)]
    public string To { get; set; }
    [Required]
    [RegularExpression("chat|groupchat")]
    public string Type { get; set; }
    [Required]
    public string Message { get; set; }
}

Problem with my code arises when one of the fields in the json request is not of the proper type; For example this sample json body (to is no longer a string):

{
    "to": 12,
    "type": "chat",
    "message": "Hi!"
}

would raise the following error:

Microsoft.AspNetCore.Http.BadHttpRequestException: Failed to read parameter "PostRequest request" from the request body as JSON.
 ---> System.Text.Json.JsonException: The JSON value could not be converted to System.String. Path: $.to | LineNumber: 1 | BytePositionInLine: 12.
 ---> System.InvalidOperationException: Cannot get the value of a token type 'Number' as a string.
   at System.Text.Json.Utf8JsonReader.GetString()
   at System.Text.Json.Serialization.Converters.StringConverter.Read(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options)
   at System.Text.Json.Serialization.Metadata.JsonPropertyInfo`1.ReadJsonAndSetMember(Object obj, ReadStack& state, Utf8JsonReader& reader)
   at System.Text.Json.Serialization.Converters.ObjectDefaultConverter`1.OnTryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value)
   at System.Text.Json.Serialization.JsonConverter`1.TryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value)
   at System.Text.Json.Serialization.JsonConverter`1.ReadCore(Utf8JsonReader& reader, JsonSerializerOptions options, ReadStack& state)
   --- End of inner exception stack trace ---
   at System.Text.Json.ThrowHelper.ReThrowWithPath(ReadStack& state, Utf8JsonReader& reader, Exception ex)
   at System.Text.Json.Serialization.JsonConverter`1.ReadCore(Utf8JsonReader& reader, JsonSerializerOptions options, ReadStack& state)
   at System.Text.Json.Serialization.JsonConverter`1.ReadCoreAsObject(Utf8JsonReader& reader, JsonSerializerOptions options, ReadStack& state)
   at System.Text.Json.JsonSerializer.ReadCore[TValue](JsonConverter jsonConverter, Utf8JsonReader& reader, JsonSerializerOptions options, ReadStack& state)
   at System.Text.Json.JsonSerializer.ReadCore[TValue](JsonReaderState& readerState, Boolean isFinalBlock, ReadOnlySpan`1 buffer, JsonSerializerOptions options, ReadStack& state, JsonConverter converterBase)
   at System.Text.Json.JsonSerializer.ContinueDeserialize[TValue](ReadBufferState& bufferState, JsonReaderState& jsonReaderState, ReadStack& readStack, JsonConverter converter, JsonSerializerOptions options)
   at System.Text.Json.JsonSerializer.ReadAllAsync[TValue](Stream utf8Json, JsonTypeInfo jsonTypeInfo, CancellationToken cancellationToken)
   at Microsoft.AspNetCore.Http.HttpRequestJsonExtensions.ReadFromJsonAsync(HttpRequest request, Type type, JsonSerializerOptions options, CancellationToken cancellationToken)
   at Microsoft.AspNetCore.Http.HttpRequestJsonExtensions.ReadFromJsonAsync(HttpRequest request, Type type, JsonSerializerOptions options, CancellationToken cancellationToken)
   at Microsoft.AspNetCore.Http.RequestDelegateFactory.<>c__DisplayClass46_3.<<HandleRequestBodyAndCompileRequestDelegate>b__2>d.MoveNext()
   --- End of inner exception stack trace ---
   at Microsoft.AspNetCore.Http.RequestDelegateFactory.Log.InvalidJsonRequestBody(HttpContext httpContext, String parameterTypeName, String parameterName, Exception exception, Boolean shouldThrow)
   at Microsoft.AspNetCore.Http.RequestDelegateFactory.<>c__DisplayClass46_3.<<HandleRequestBodyAndCompileRequestDelegate>b__2>d.MoveNext()
--- End of stack trace from previous location ---
   at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)

HEADERS
=======
Accept: */*
Connection: keep-alive
Host: localhost:5090
User-Agent: PostmanRuntime/7.26.8
Accept-Encoding: gzip, deflate, br
Content-Type: application/json
Content-Length: 62
Postman-Token: e31d3575-d2ec-49a7-9bef-04eaecf38a24

Obviously it no longer can cast request into an object of type PostRequest but what is the proper way to handle these kind of situations? (defining request as type object and checking for the presence and type of every property seems ugly)

Further description: I want to now how I can catch the above mentioned error.

CodePudding user response:

Asp.Net Core provides a way to register exception handlers:

app.UseExceptionHandler(c => c.Run(async context =>
{
    var exception = context.Features
        .Get<IExceptionHandlerFeature>()
        ?.Error;
    if (exception is not null)
    {
        var response = new { error = exception.Message };
        context.Response.StatusCode = 400;

        await context.Response.WriteAsJsonAsync(response);
    }
}));

CodePudding user response:

Minimal API is called that way for a reason - many convenient features related to binding, model state and so on are not present compared to MVC in sake of simplicity and performance.

Maybe there is more convenient approach to your task but for example you can leverage custom binding mechanism to combine the json parsing and validation:

public class ParseJsonAndValidationResult<T>
{
    public T? Result { get; init; }
    public bool Success { get; init; }
    // TODO - add errors

    public static async ValueTask<ParseJsonAndValidationResult<T>?> BindAsync(HttpContext context)
    {
        try
        {
            var result = await context.Request.ReadFromJsonAsync<T>(context.RequestAborted);
            var validationResults = new List<ValidationResult>();
            if (!Validator.TryValidateObject(result, new ValidationContext(result), validationResults, true))
            {
                // TODO - add errors
                return new ParseJsonAndValidationResult<T>
                {
                    Success = false
                };
            }

            return new ParseJsonAndValidationResult<T>
            {
                Result = result,
                Success = true
            };
        }
        catch (Exception ex)
        {
            // TODO - add errors
            return new ParseJsonAndValidationResult<T>
            {
                Success = false
            };
        }

    }
}

And in the MapPost:

app.MapPost("/API_v1/Send", (ParseJsonAndValidationResult<PostRequest> request) =>
{ 
    // analyze the result ...
});
  • Related