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 ...
});