XML Serialization - Required attribute - asp.net-core

I have a class that should represent a controller's action parameter and I'd like its properties to be "required" (meaning, you get a status code 400 or something in case it's passed as null). I managed to get it done using System.ComponentModel.DataAnnotations, but the ErrorMessage that I pass to the constructor of the Required attribute is never shown.
[XmlRoot(ElementName = "root")]
public class Request
{
[XmlElement(ElementName = "prop")]
[Required(ErrorMessage = "The property is required.")]
public string Property { get; set; }
[XmlElement(ElementName = "another")]
[Required(ErrorMessage = "The property is required.")]
public string Another { get; set; }
}
Action:
[HttpPost]
public IActionResult Post([FromBody] Request value)
{
return Ok(value); //ignore this, it's just for testing purposes...
}
However, if I don't pass the Property value, I get a 400 that doesn't contain the ErrorMessage I passed earlier. Am I missing something here?
<ValidationProblemDetails xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Title>One or more validation errors occurred.</Title>
<Status>400</Status>
</ValidationProblemDetails>
My Startup has Xml formatters added to it:
services.AddMvc(options =>
{
options.RespectBrowserAcceptHeader = true;
options.InputFormatters.Insert(0, new XmlSerializerInputFormatter(options));
options.OutputFormatters.Insert(0, new XmlSerializerOutputFormatter());
}).SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
The body of the request looks like this, and it doesn't have "Property":
<root>
<another>Test</another>
<!-- Property "Property" is missing here -->
</root>

Kudos to Code Rethinked for the huge help - Customizing automatic HTTP 400 error response in ASP.NET Core Web APIs.
An approach that I managed to figure out eventually includes the use of services.Configure in my Startup.ConfigureServices method.
services.Configure<ApiBehaviorOptions>(options =>
{
options.InvalidModelStateResponseFactory = context =>
{
return new OkObjectResult(new CustomResponse(someStatusCode, context))
{
ContentTypes = { "application/xml" }
};
};
});
So, I made a class named CustomResponse that holds the status code I want to retrieve and all the validation errors (including the ones where my Required property was not passed to the API).
[XmlRoot(ElementName = "rcemsTrxSubReqAck")]
public class CustomResponse
{
[XmlElement(ElementName = "Status")]
public string Status { get; set; }
[XmlArray(ElementName = "Errors"), XmlArrayItem(ElementName = "Error")]
public string[] Errors { get; set; }
public CustomResponse(int status, ActionContext context)
{
Status = status;
Errors = ConstructErrorMessages(context);
}
private string[] ConstructErrorMessages(ActionContext context)
{
if (context == null)
{
return null;
}
string[] arr = new string[context.ModelState.ErrorCount];
int i = 0;
foreach (var keyModelStatePair in context.ModelState)
{
var key = keyModelStatePair.Key;
var errors = keyModelStatePair.Value.Errors;
if (errors != null && errors.Count > 0)
{
if (errors.Count == 1)
{
var errorMessage = GetErrorMessage(errors[0]);
arr[i] = $"{key}: {errorMessage}";
}
else
{
var errorMessages = new string[errors.Count];
for (var j = 0; j < errors.Count; j++)
{
errorMessages[j] = GetErrorMessage(errors[j]);
}
arr[i] = $"{key}: {errorMessages.ToString()}";
}
i++;
}
}
return arr;
}
private string GetErrorMessage(ModelError error)
{
return string.IsNullOrEmpty(error.ErrorMessage) ? "The input was not valid." : error.ErrorMessage;
}
}

Related

Display fields (from query) on Swagger UI from complex record

I have a complex record SearchProductsRequest in a GET request that receives the parameters by query (
/v1/products?ids=1,2,3&name=hombre&page=3&pageItems=4&sortField=name&sort=asc ).
app.MapGet(
$"/{ProductCatalogueApi.Version}/products",
(SearchProductsRequest request)
=> ProductApiDelegates.SearchProducts(
request));
In the record, I've implemented the bind async
public static ValueTask<SearchProductsRequest?> BindAsync(HttpContext httpContext, ParameterInfo parameter); and now the parameters from the URL automatically convert the parameters to SearchProductsRequest.
The request is working as intended, but we are using (Swashbuckle -> ) Swagger UI for development.
Swagger UI does not recognize the members from SearchProductsRequest to display them as input boxes. Is there a way to make swagger UI know them and display them so a user consulting the swagger endpoint can pass value through it?
I was hoping to get the following:
Until now, I've only managed to have the fields displayed in swagger if I have all of them in the Map.Get() explicitly.
EDIT:
Adding asked content
Record:
public record SearchProductsRequest
{
public IEnumerable<int>? Ids { get; private set; }
public string? Name { get; private set; }
public PaginationInfoRequest? PaginationInfo { get; private set; }
public SortingInfoRequest? SortingInfo { get; private set; }
public SearchProductsRequest(
IEnumerable<int>? ids,
string? name,
PaginationInfoRequest? PaginationInfo,
SortingInfoRequest? SortingInfo)
{
this.Ids = ids;
this.Name = name;
this.PaginationInfo = PaginationInfo;
this.SortingInfo = SortingInfo;
}
public static ValueTask<SearchProductsRequest?> BindAsync(
HttpContext httpContext,
ParameterInfo parameter)
{
var ids = ParseIds(httpContext);
var name = httpContext?.Request.Query["name"] ?? string.Empty;
PaginationInfoRequest? pagination = null;
SortingInfoRequest? sorting = null;
if (int.TryParse(httpContext?.Request.Query["page"], out var page)
&& int.TryParse(httpContext?.Request.Query["pageItems"], out var pageItems))
{
pagination = new PaginationInfoRequest(page, pageItems);
}
var sortField = httpContext?.Request.Query["sortField"].ToString();
if (!string.IsNullOrEmpty(sortField))
{
sorting = new SortingInfoRequest(
sortField,
httpContext?.Request.Query["sort"].ToString() == "asc");
}
return ValueTask.FromResult<SearchProductsRequest?>(
new SearchProductsRequest(
ids,
name!,
pagination,
sorting));
}
#pragma warning disable SA1011 // Closing square brackets should be spaced correctly
private static int[]? ParseIds(HttpContext httpContext)
{
int[]? ids = null;
var commaSeparatedIds = httpContext?.Request.Query["ids"]
.ToString();
if (!string.IsNullOrEmpty(commaSeparatedIds))
{
ids = commaSeparatedIds
.Split(",")
.Select(int.Parse)
.ToArray() ?? Array.Empty<int>();
}
return ids;
}
#pragma warning restore SA1011 // Closing square brackets should be spaced correctly
}
Delegate:
internal static async Task<IResult> SearchProducts(
ILogger<ProductApiDelegates> logger,
IMapper mapper,
SearchProductsRequest request,
IValidator<SearchProductsRequest> validator,
IProductService productService)
{
using var activity = s_activitySource.StartActivity("Search products");
var validationResult = await validator.ValidateAsync(request);
if (!validationResult.IsValid)
{
var errors = validationResult.GetErrors();
logger.LogError("Bad Request: {Errors}", errors);
return Results.BadRequest();
}
try
{
logger.LogInformation("Searching product details by name");
var filtersContainer = mapper.Map<SearchProductsFiltersContainer>(request);
var products = await productService.SearchProductsAsync(filtersContainer);
if (products == null)
{
return Results.NotFound();
}
var searchProducts = BuildSearchProducts(mapper, products);
var paginationInfo = await BuildPaginationInfo(filtersContainer, productService);
var response = new SearchProductsResponse(searchProducts, paginationInfo);
return Results.Ok(response);
}
catch (Exception ex)
{
logger.LogError(ex, "Error searching the products");
return Results.Problem();
}
}

Elasticsearch NEST - System.StackOverflowException when running Index

When I run this code:
var result = _client.Index<EntityType>(item, i => i.Index(n));
I'm getting this error:
Exception has occurred: CLR/System.StackOverflowException An unhandled
exception of type 'System.StackOverflowException' occurred in
Elasticsearch.Net.dll
The full method:
public bool Index<EntityType>(EntityType item, int attempt = 0) where EntityType : class, IDomainEntity<int>
{
const int maxRetries = 5;
if (item == null)
{
return false;
}
var type = item.GetType();
var attributes = type.CustomAttributes;
string n = "";
foreach (var attribute in attributes)
{
foreach (var arg in attribute.NamedArguments)
{
if (arg.MemberName == "RelationName")
{
n = arg.TypedValue.Value.ToString().ToLower();
}
}
}
var result = _client.Index<EntityType>(item, i => i.Index(n));
if (!CheckResponse(result) && attempt < maxRetries)
{
RefreshClient<EntityType>();
attempt++;
return Index(item, attempt);
}
RefreshClient<EntityType>();
return result.IsValid;
}
I added [PropertyName("propertyToIgnoreInElasticsearch", Ignore = true)] from NEST to my POCO fields which were causing an infinite loop while Indexing. It ignores a field from the Elasticsearch Index so it is not indexed.
for example:
[Serializable]
public abstract class VeganItem<VeganItemEstablishmentType>
{
[Required]
public string Name { get; set; }
[PropertyName("veganItemEstablishments", Ignore = true)]
public virtual ICollection<VeganItemEstablishmentType> VeganItemEstablishments { get; set; }
}

FluentValidation failure not returning BadRequest

I have wired up FluentValidation as per instructions, and when debuging test I can see that model is invalid based on the test setup, but exception is not thrown, but rather method on the controller is being executed. This is on 3.1 with EndPoint routing enabled. Is there anything else one needs to do to get this to work and throw. What happens is that validation obviously runs; it shows as ModelState invalid and correct InstallmentId is invalid, but it keeps processing in Controller instead of throwing exception.
services.AddMvc(
options =>
{
options.EnableEndpointRouting = true;
//// options.Filters.Add<ExceptionFilter>();
//// options.Filters.Add<CustomerRequestFilter>();
})
.AddFluentValidation(
config =>
{
config.RegisterValidatorsFromAssemblyContaining<Startup>();
})
Command and Validator
public class ProcessManualPayment
{
public class Command
: CustomerRequest<Result?>
{
public Guid PaymentPlanId { get; set; }
public Guid InstallmentId { get; set; }
public Guid PaymentCardId { get; set; }
}
public class Validator : AbstractValidator<Command>
{
public Validator()
{
this.RuleFor(x => x.CustomerId)
.IsValidGuid();
this.RuleFor(x => x.PaymentPlanId)
.IsValidGuid();
this.RuleFor(x => x.InstallmentId)
.IsValidGuid();
this.RuleFor(x => x.PaymentCardId)
.IsValidGuid();
}
}
Controller
[Authorize]
[HttpPost]
[Route("payments")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> ProcessManualPayment(
[FromBody]
ProcessManualPayment.Command command)
{
Test
[Fact]
public async Task When_Command_Has_Invalid_Payload_Should_Fail()
{
var client = this.factory.CreateClient();
// Arrange
var validCmd = new ProcessManualPayment.Command()
{
CustomerId = Guid.NewGuid(),
PaymentPlanId = Guid.NewGuid(),
InstallmentId = Guid.NewGuid(),
PaymentCardId = Guid.NewGuid(),
};
var validCmdJson = JsonConvert.SerializeObject(validCmd, Formatting.None);
var jObject = JObject.Parse(validCmdJson);
jObject["installmentId"] = "asdf";
var payload = jObject.ToString(Formatting.None);
// Act
var content = new StringContent(payload, Encoding.UTF8, MediaTypeNames.Application.Json);
var response = await client.PostAsync(MakePaymentUrl, content);
var returned = await response.Content.ReadAsStringAsync();
response.StatusCode.ShouldBe(HttpStatusCode.BadRequest);
}
[Fact]
public async Task When_Payload_Is_Null_Should_Fail()
{
// Arrange
var client = this.factory.CreateClient();
// Act
var response = await client.PostAsJsonAsync(MakePaymentUrl, null);
// Assert
response.StatusCode.ShouldBe(HttpStatusCode.BadRequest);
}
GuidValidator
public class GuidValidator : PropertyValidator
{
public GuidValidator()
: base("'{PropertyName}' value {AttemptedValue} is not a valid Guid.")
{
}
protected override bool IsValid(PropertyValidatorContext context)
{
context.MessageFormatter.AppendArgument("AttemptedValue", context.PropertyValue ?? "'null'");
if (context.PropertyValue == null)
{
return false;
}
Guid.TryParse(context.PropertyValue.ToString(), out var value);
return IsValid(value);
}
private static bool IsValid(Guid? value) =>
value.HasValue
&& !value.Equals(Guid.Empty);
}
Mystery solved, I was missing [ApiController] attribute on the controller.

MVC WebAPI Data Annotation Error Message Empty String

I have implemented an OWIN self-hosted webapi and am trying to use data annotations and an ActionFilterAttribute to return formatted errors to the user. I have set custom error messages on the data annotation but when I try to retrieve the message from the ModelState it is always an empty string (shown in image below).
Model:
public class JobPointer
{
[Required(ErrorMessage = "JobId Required")]
public Guid JobId { get; set; }
}
Filter:
public class ModelValidationFilter : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
if (actionContext.ModelState.IsValid) return;
string errors = actionContext.ModelState.SelectMany(state => state.Value.Errors).Aggregate("", (current, error) => current + (error.ErrorMessage + ". "));
actionContext.Response = actionContext.Request.CreateErrorResponse(
HttpStatusCode.BadRequest, errors);
}
}
Endpoint:
[HttpPost]
public HttpResponseMessage DescribeJob(JobPointer jobId)
{
Job job = _jobhelper.GetJob(jobId.JobId);
return Request.CreateResponse(HttpStatusCode.OK, job);
}
Request Body:
{
}
Response:
Status Code: 400
{
"Message": ". "
}
If I change error.Message in ModelValidationFilter to error.Exception.Message I get back the default validation error:
Status Code: 400
{
"Message": "Required property 'JobId' not found in JSON. Path '', line 3, position 2.. "
}
I know this is an old question, but I just had this problem and found the solution myself.
As you no doubt discovered, as Guid is a non-nullable type [Required] produces an unfriendly error message (I assume because the JSON parser picks it up before actually getting the model validation).
You can get around this by making the Guid nullable...
public class JobPointer
{
[Required(ErrorMessage = "JobId Required")]
public Guid? JobId { get; set; }
}
... however, this is not a viable option in all cases (as in my case), so I ended up writing my own validation attribute that would check the property against it's Empty declaration...
public class IsNotEmptyAttribute : ValidationAttribute
{
public override bool IsValid(object value)
{
if (value == null) return false;
var valueType = value.GetType();
var emptyField = valueType.GetField("Empty");
if (emptyField == null) return true;
var emptyValue = emptyField.GetValue(null);
return !value.Equals(emptyValue);
}
}
You could then implement like...
public class JobPointer
{
[IsNotEmpty(ErrorMessage = "JobId Required")]
public Guid JobId { get; set; }
}

Custom model binder with inheritance using Web API and RavenDB

I'm developing a simple web app where I need to bind all types implementing and interface of a specific type. My interface has one single property like this
public interface IContent {
string Id { get;set; }
}
a common class using this interface would look like this
public class Article : IContent {
public string Id { get;set; }
public string Heading { get;set; }
}
to be clean here the article class is just one of many different classes implementing IContent so therefor I need a generic way of storing and updating these types.
So in my controller I have the put method like this
public void Put(string id, [System.Web.Http.ModelBinding.ModelBinder(typeof(ContentModelBinder))] IContent value)
{
// Store the updated object in ravendb
}
and the ContentBinder
public class ContentModelBinder : System.Web.Http.ModelBinding.IModelBinder {
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext) {
actionContext.ControllerContext.Request.Content.ReadAsAsync<Article>().ContinueWith(task =>
{
Article model = task.Result;
bindingContext.Model = model;
});
return true;
}
}
The code above does not work because it does not seem to get hold of the Heading property even though if I use the default model binder it binds the Heading correctly.
So, in the BindModel method I guess I need to load the correct object from ravendb based on the Id and then update the complex object using some kind of default model binder or so? This is where I need some help.
Marcus, following is an example which would work fine for both Json and Xml formatter.
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Net.Http.Formatting;
using System.Net.Http.Headers;
using System.Runtime.Serialization;
using System.Web.Http;
using System.Web.Http.SelfHost;
namespace Service
{
class Service
{
private static HttpSelfHostServer server = null;
private static string baseAddress = string.Format("http://{0}:9095/", Environment.MachineName);
static void Main(string[] args)
{
HttpSelfHostConfiguration config = new HttpSelfHostConfiguration(baseAddress);
config.Routes.MapHttpRoute("Default", "api/{controller}/{id}", new { id = RouteParameter.Optional });
config.IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.Always;
config.Formatters.JsonFormatter.SerializerSettings.TypeNameHandling = TypeNameHandling.Objects;
try
{
server = new HttpSelfHostServer(config);
server.OpenAsync().Wait();
Console.WriteLine("Service listenting at: {0} ...", baseAddress);
TestWithHttpClient("application/xml");
TestWithHttpClient("application/json");
Console.ReadLine();
}
catch (Exception ex)
{
Console.WriteLine("Exception Details:\n{0}", ex.ToString());
}
finally
{
if (server != null)
{
server.CloseAsync().Wait();
}
}
}
private static void TestWithHttpClient(string mediaType)
{
HttpClient client = new HttpClient();
MediaTypeFormatter formatter = null;
// NOTE: following any settings on the following formatters should match
// to the settings that the service's formatters have.
if (mediaType == "application/xml")
{
formatter = new XmlMediaTypeFormatter();
}
else if (mediaType == "application/json")
{
JsonMediaTypeFormatter jsonFormatter = new JsonMediaTypeFormatter();
jsonFormatter.SerializerSettings.TypeNameHandling = TypeNameHandling.Objects;
formatter = jsonFormatter;
}
HttpRequestMessage request = new HttpRequestMessage();
request.RequestUri = new Uri(baseAddress + "api/students");
request.Method = HttpMethod.Get;
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(mediaType));
HttpResponseMessage response = client.SendAsync(request).Result;
Student std = response.Content.ReadAsAsync<Student>().Result;
Console.WriteLine("GET data in '{0}' format", mediaType);
if (StudentsController.CONSTANT_STUDENT.Equals(std))
{
Console.WriteLine("both are equal");
}
client = new HttpClient();
request = new HttpRequestMessage();
request.RequestUri = new Uri(baseAddress + "api/students");
request.Method = HttpMethod.Post;
request.Content = new ObjectContent<Person>(StudentsController.CONSTANT_STUDENT, formatter);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(mediaType));
Student std1 = client.SendAsync(request).Result.Content.ReadAsAsync<Student>().Result;
Console.WriteLine("POST and receive data in '{0}' format", mediaType);
if (StudentsController.CONSTANT_STUDENT.Equals(std1))
{
Console.WriteLine("both are equal");
}
}
}
public class StudentsController : ApiController
{
public static readonly Student CONSTANT_STUDENT = new Student() { Id = 1, Name = "John", EnrolledCourses = new List<string>() { "maths", "physics" } };
public Person Get()
{
return CONSTANT_STUDENT;
}
// NOTE: specifying FromBody here is not required. By default complextypes are bound
// by formatters which read the body
public Person Post([FromBody] Person person)
{
if (!ModelState.IsValid)
{
throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest, this.ModelState));
}
return person;
}
}
[DataContract]
[KnownType(typeof(Student))]
public abstract class Person : IEquatable<Person>
{
[DataMember]
public int Id { get; set; }
[DataMember]
public string Name { get; set; }
// this is ignored
public DateTime DateOfBirth { get; set; }
public bool Equals(Person other)
{
if (other == null)
return false;
if (ReferenceEquals(this, other))
return true;
if (this.Id != other.Id)
return false;
if (this.Name != other.Name)
return false;
return true;
}
}
[DataContract]
public class Student : Person, IEquatable<Student>
{
[DataMember]
public List<string> EnrolledCourses { get; set; }
public bool Equals(Student other)
{
if (!base.Equals(other))
{
return false;
}
if (this.EnrolledCourses == null && other.EnrolledCourses == null)
{
return true;
}
if ((this.EnrolledCourses == null && other.EnrolledCourses != null) ||
(this.EnrolledCourses != null && other.EnrolledCourses == null))
return false;
if (this.EnrolledCourses.Count != other.EnrolledCourses.Count)
return false;
for (int i = 0; i < this.EnrolledCourses.Count; i++)
{
if (this.EnrolledCourses[i] != other.EnrolledCourses[i])
return false;
}
return true;
}
}
}
I used #kiran-challa solution and added TypeNameHandling on Json media type formatter's SerializerSettings.