Following is the Model class that I use for the application
public class APIVesselFilter
{
[Required(ErrorMessage = "Vessel is required")]
public Guid VesselID { get; set; }
public Guid? AnchorageID { get; set; }
}
Following is the Validation Filter that will check if the ModelState is Valid and if not valid I will send the error message.
public class ValidateModelAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
if (actionContext.ModelState.IsValid == false)
{
List<string> errorList = new List<string>();
foreach (var err in actionContext.ModelState.Values)
{
foreach (var er in err.Errors)
{
errorList.Add(er.ErrorMessage);
}
}
actionContext.Response = actionContext.Request.CreateResponse(APIResponse.SendResponse(RequestStatus.GetValidationFailedMessage(), actionContext.ModelState));
}
}
}
Here, Following is the response that I get using the above Model State. Over here the error message is not in a user understandable way and so I have added the ErrorMessage in Require attribute of Vessel and I loop through the errors in ModelState. But my error message is always empty (I checked this using a debugger). What am I missing here so that the error message will be bound directly to the ModelState?
{
"Status": {
"StatusCode": 620,
"StatusMessage": "Validation Failed"
},
"Data": {
"filter": {
"_errors": [
{
"<Exception>k__BackingField": {
"ClassName": "Newtonsoft.Json.JsonSerializationException",
"Message": "Required property 'VesselID' not found in JSON. Path '', line 1, position 56.",
"Data": null,
"InnerException": null,
"HelpURL": null,
"StackTraceString": " at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.EndObject(Object newObject, JsonReader reader, JsonObjectContract contract, Int32 initialDepth, Dictionary`2 propertiesPresence)",
"RemoteStackTraceString": null,
"RemoteStackIndex": 0,
"ExceptionMethod": "8\nEndObject\nNewtonsoft.Json, Version=6.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed\nNewtonsoft.Json.Serialization.JsonSerializerInternalReader\nVoid EndObject(System.Object, Newtonsoft.Json.JsonReader, Newtonsoft.Json.Serialization.JsonObjectContract, Int32, System.Collections.Generic.Dictionary`2[Newtonsoft.Json.Serialization.JsonProperty,Newtonsoft.Json.Serialization.JsonSerializerInternalReader+PropertyPresence])",
"HResult": -2146233088,
"Source": "Newtonsoft.Json",
"WatsonBuckets": null
},
"<ErrorMessage>k__BackingField": ""
}
],
"<Value>k__BackingField": null
}
}
}
Try decorating your model property with a DisplayAttribute:
public class APIVesselFilter
{
[Required]
[Display(Name="Vessel is required")]
public Guid VesselID { get; set; }
public Guid? AnchorageID { get; set; }
}
I know this is the way to customise your messages when using the Html.ValidationSummary and a quick test showed this comes up when inspecting the ModelState in an action.
using CreateErrorResponse instead of CreateResponse might solve your problem.
I faced a smiliar problem and was fixed with that.
In your case this will be
actionContext.Response = actionContext.Request.CreateErrorResponse(APIResponse.SendResponse(RequestStatus.GetValidationFailedMessage(), actionContext.ModelState));
Related
When calling an HTTP get method, the query parameters are not correctly filled in.
Let me explain with the code.
[HttpGet]
public async Task<IActionResult> GetTasks(Guid propertyId, [FromQuery] TaskFilterParams filterParams, [FromQuery] SortingParams sortingParams, [FromQuery] PagingParams pagingParams)
{
// Do some stuff, not important
}
So we call this endpoint with the following url: someurl.com/api/v1/properties/[some-guid]/tasks?&roomClass=Single&category=Cleaning
As you can see, we only have provided three values, a GUID, a type of room and the category of the task. The sorting and paging parameters are empty.
The problem is that the 'category' parameter is not filled in. It is always null, while the other parameters are filled in correctly. No matter what we try (url encoding, re-arranging the order of the parameters, ...).
The real catch is however, if we restart the service, it sometimes works for a while. But every so often it fails and we need to restart evertything again.
Has anyone encountered the same issue? Or, even better, a solution?
PS: The code is running in a docker container in AKS (FROM mcr.microsoft.com/dotnet/core/aspnet:3.1).
[DBG] Get tasks parameters: PropertyId: [some-guid], TaskFilterParams:{"Category": null, "RoomClass": {"Value": "Single", "Operator": "Equal", "$type": "FilterParam`1"}, "$type": "TaskFilterParams"}
public class TaskFilterParams
{
public FilterParam<IssueCategory> Category { get; set; }
public FilterParam<RoomClass> RoomClass { get; set; }
public FilterParam<Guid?> Room { get; set; }
}
public abstract class FilterParam
{
public FilterOperator Operator { get; set; }
}
[TypeDescriptionProvider(typeof(FilterParamTypeDescriptionProvider))]
[JsonObject]
public class FilterParam<T> : FilterParam
{
public T Value { get; set; }
public static bool TryParse(string s, out FilterParam<T> result)
{
result = default(FilterParam<T>);
var filterOperators = Enum.GetValues(typeof(FilterOperator)).Cast<FilterOperator>();
foreach (var filterOperatorString in filterOperators.Select<FilterOperator, string>(x => x.ToFilterOperatorString()).OrderByDescending(x => x.Length))
{
if (s.StartsWith(filterOperatorString))
{
if (s.TryParse(filterOperatorString, out result))
{
return true;
}
return false;
}
}
return false;
}
}
public enum IssueCategory
{
Cleaning = 0,
Inspection = 1,
Maintenance = 2
}
public enum RoomClass
{
Single = 0,
Double = 1,
Family = 2
}
UPDATE:
We have added more logging. It seems the data is present in the query string but the model is not binding correctly.
Log.Debug("Query: {#query}", HttpContext.Request.Query);
Log.Debug(
"Get tasks parameters: PropertyId:{#propertyId}, TaskFilterParams:{#filterParams}, SortingParams:{#sortingParams}, PagingParams:{#pagingParams}",
propertyId, filterParams, sortingParams, pagingParams);
[16:51:35 DBG] Query: [{"Key": "category", "Value": ["Cleaning"], "$type": "KeyValuePair`2"}, {"Key": "roomClass", "Value": ["Single"], "$type": "KeyValuePair`2"}]
[16:51:35 DBG] Get tasks parameters: PropertyId:[some-guid], TaskFilterParams:{"Category": null, "RoomClass": {"Value": "Single", "Operator": "Equal", "$type": "FilterParam`1"}, "$type": "TaskFilterParams"}
Let's say I have the following input model:
public class InputModel
{
[Required]
public string? Name { get; set; }
[Required]
public DateTime? Birthday { get; set; }
}
When the birthday field is not provided then I get the following appropriate response:
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "00-818caf3d757ae345a735fd0f4a523ecb-e9f90641c111814c-00",
"errors": {
"Birthday": [
"The Birthday field is required."
]
}
}
But if I provide an invalid date string, then the following is returned:
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "00-715471c843155940a6f0cae580cd1b69-247e6dbfe3442446-00",
"errors": {
"model": [
"The model field is required."
],
"$.birthday": [
"The JSON value could not be converted to System.Nullable`1[System.DateTime]. Path: $.birthday | LineNumber: 13 | BytePositionInLine: 37."
]
}
}
These two response models are not consistent which makes it difficult for the client to reason about the validation errors.
How can I validate the DateTime string before it gets handled by the converter so that I can return a response model similar to the first? Something like this:
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "00-818caf3d757ae345a735fd0f4a523ecb-e9f90641c111814c-00",
"errors": {
"Birthday": [
"The Birthday field is badly formed."
]
}
}
How can I validate the DateTime string before it gets handled by the
converter so that I can return a response model similar to the first?
You can create a custom BadRequest method that inherits ValidationProblemDetails to return the error message you want.
First, add the following code in your startup.cs ConfigureServices method:
services.AddMvc()
.ConfigureApiBehaviorOptions(options =>
{
options.InvalidModelStateResponseFactory = context =>
{
var problems = new CustomBadRequest(context);
return new BadRequestObjectResult(problems);
};
});
Here is the custom Bad Request method:
public class CustomBadRequest : ValidationProblemDetails
{
public CustomBadRequest(ActionContext context)
{
Title = "Invalid arguments to the API";
Detail = "The inputs supplied to the API are invalid";
Status = 400;
ConstructErrorMessages(context);
Type = context.HttpContext.TraceIdentifier;
}
private void ConstructErrorMessages(ActionContext context)
{
foreach (var keyModelStatePair in context.ModelState)
{
var key = keyModelStatePair.Key.Replace("$.", "");
var errors = keyModelStatePair.Value.Errors;
if (errors != null && errors.Count > 0)
{
if (errors.Count == 1)
{
var errorMessage = GetErrorMessage(key, errors[0]);
Errors.Add(key, new[] { errorMessage });
}
else
{
var errorMessages = new string[errors.Count];
for (var i = 0; i < errors.Count; i++)
{
errorMessages[i] = GetErrorMessage(key,errors[i]);
}
Errors.Add(key, errorMessages);
}
}
}
}
string GetErrorMessage(string key, ModelError error)
{
if (error.ErrorMessage != $"The {key} field is required.")
{
return $"The {key} field is badly formed.";
}
return error.ErrorMessage;
}
}
After the above settings, when your InputModel is illegal, it will automatically enter the CustomBadRequest method and return the corresponding error message through judgment.
Here is the test result through postman:
Try defining the datatype of the datetime field like as follows:
[Required]
[DataType(DataType.DateTime)]
public DateTime? Birthday { get; set; }
I'm working on a webapi project using .netcore.
I have a model with the following properties:
public class Criterial {
[Required]
public string Field { get; set; }
[Required]
public Operator Operator { get; set; }
[Required]
public string Value { get; set; }
public bool Result { get; set; }
}
public enum Operator {
greater_than,
equal_to,
lower_than
}
I'm trying to use enum to restrict the values that the Operator propertie can receive, but when I make a POST request to the API I got the following scenario:
POST Request Body:
"criterials": [
{
"field": "amount",
"operator": "greater_than",
"value": "50"
}
]
Response from the API:
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "|7e53377-444fa4a723ac655c.",
"errors": {
"$.criterials[0].operator": [
"The JSON value could not be converted to LeagueOfFateApi.Models.Operator. Path: $.criterials[0].operator | LineNumber: 5 | BytePositionInLine: 26."
]
}
}
Searching about the issue on the internet I found the [JsonConverter(typeof(JsonStringEnumConverter))] Data Annotation.
So I added it to my code and the issue was "solved":
[Required]
[JsonConverter(typeof(JsonStringEnumConverter))]
public Operator Operator { get; set; }
New response from the API:
"criterials": [
{
"field": "amount",
"operator": "greater_than",
"value": "50",
"result": false
}
]
The problem is: in my MongoDB collection a new document was saved with the int value 0 of the enums, and not the string value "greater_than":
"Criterials" : [
{
"Field" : "amount",
"Operator" : 0,
"Value" : "50",
"Result" : false
}
]
Besides, another problem is that the "criterial" field can receive any int value with no restrictions.
Is there any other practical way to restrict a string's options without using enums? Or is there anything I can add to this solution using enums?
Thank you very much for your attention and your time!
According to your description, I suggest you could write custom set and get method for the Operator property.
You could set the Operator's type is string and use Enum.IsDefined to check the Operator value is enum Operator or not.
More details, you could refer to below codes:
public class Criterial
{
[Required]
public string Field { get; set; }
private string _Operator;
[Required]
public string Operator {
get {
return this._Operator;
}
set {
if (Enum.IsDefined(typeof(Operator), value))
{
this._Operator = value;
}
else
{
this._Operator = "Error you used wrong string";
}
}
}
[Required]
public string Value { get; set; }
public bool Result { get; set; }
}
public enum Operator
{
greater_than,
equal_to,
lower_than
}
Result:
I have a JSON response that I would like to parse using JSON.NET. I have done this with single values before but never when the response could contain an object that consist of an array as the errors property does below.
{
"code": "InvalidObject",
"message": "payment object is invalid",
"errors": [
{
"code": "AccountingApi",
"message": "Paid amount cannot be greater than the amount of the invoice.",
"resource": "payment",
"field": "amount"
},
{
"code": "AccountingApi",
"message": "Payment has not been verified",
"resource": "payment",
"field": "verification"
}
]
}
I would like to extract the error messages into a List. How do I specify that I want to grab the message property in the errors collection?
List<string> errorMessages = parsedJson["errors"].ToList<string>();
You could use
class Error
{
public string code { get; set; }
public string message { get; set; }
public string resource { get; set; }
public string field { get; set; }
}
class Some
{
public string code { get; set; }
public string message { get; set; }
public List<Error> errors { get; set; }
}
Then (Probably you'll send your json string as param )
List<string> parse()
{
var s = new StringBuilder();
s.Append("{");
s.Append(" \"code\": \"InvalidObject\",");
s.Append("\"message\": \"payment object is invalid\",");
s.Append("\"errors\": [");
s.Append("{");
s.Append("\"code\": \"AccountingApi\",");
s.Append("\"message\": \"Paid amount cannot be greater than the amount of the invoice.\",");
s.Append("\"resource\": \"payment\",");
s.Append("\"field\": \"amount\"");
s.Append("},");
s.Append("{");
s.Append("\"code\": \"AccountingApi\",");
s.Append("\"message\": \"Payment has not been verified\",");
s.Append("\"resource\": \"payment\",");
s.Append("\"field\": \"verification\" ");
s.Append("}");
s.Append("]");
s.Append("}");
var json = s.ToString();
var obj = JsonConvert.DeserializeObject<Some>(json);
return obj.errors.Select(x => x.message).ToList();
}
Is it possible to skip a part of json when contract type does not match. Namely, I receive a json response which does not conform the schema - null objects are serialized as empty arrays ("thank's" to php serializer). So, instead of "null" or "{}" I get "[]".
So far, I tried to use Error event handler available in JsonSerializerSettings. However, this approach is rather ugly because Json.net does not return meaningful exception type.
Example of valid response:
{
"key": "key",
"id": "id",
"status": "status",
"opts": {
"start": 42,
"limit": 12
}
}
}
Example of invalid response
{
"key": "key",
"id": "id",
"status": "status",
"opts": []
}
You can handle this issue using a custom JsonConverter. The converter can detect the format of the opts property in the JSON and then either deserialize it or set its value to null in the target object. Here's how I would write the converter:
class OptsConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return (objectType == typeof(Opts));
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
JToken token = JToken.Load(reader);
if (token.Type == JTokenType.Object)
{
return new Opts
{
start = (int)token["start"],
limit = (int)token["limit"]
};
}
return null;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
Here is a demo:
class Program
{
static void Main(string[] args)
{
string valid = #"
{
""key"": ""key"",
""id"": ""id"",
""status"": ""status"",
""opts"": {
""start"": 42,
""limit"": 12
}
}";
DeserializeAndWriteToConsole(valid);
string invalid = #"
{
""key"": ""key"",
""id"": ""id"",
""status"": ""status"",
""opts"": []
}";
DeserializeAndWriteToConsole(invalid);
}
private static void DeserializeAndWriteToConsole(string json)
{
RootObject root = JsonConvert.DeserializeObject<RootObject>(json, new OptsConverter());
Console.WriteLine("key: " + root.key);
Console.WriteLine("id: " + root.id);
Console.WriteLine("status: " + root.status);
if (root.opts != null)
{
Console.WriteLine("opts.start: " + root.opts.start);
Console.WriteLine("opts.limit: " + root.opts.limit);
}
else
{
Console.WriteLine("opts: (null)");
}
Console.WriteLine();
}
}
public class RootObject
{
public string key { get; set; }
public string id { get; set; }
public string status { get; set; }
public Opts opts { get; set; }
}
public class Opts
{
public int start { get; set; }
public int limit { get; set; }
}
Output:
key: key
id: id
status: status
opts.start: 42
opts.limit: 12
key: key
id: id
status: status
opts: (null)