I am working on an API that needs to return an array of enums. I am using JSON.NET with WebAPI, and while the StringEnumConverter is sufficient to convert properties which are enums into their string values, it doesn't convert a result which is just an array of enums, instead it returns just the integer value.
So if my endpoint looks like this:
[RoutePrefix("Items")]
public class ItemsController : ApiController
{
[HttpGet][Route("")]
public IHttpActionResult Get()
{
var items = Enum.GetValues(typeof(Items))
.Cast<Items>()
.ToList()
return Ok(items);
}
}
public enum Items
{
First = 0,
Second = 1,
Third = 2
}
Then a call to GET /Items currently returns [ 0, 1, 2 ]; what I would like to get back is [ "First", "Second", "Third" ].
What I don't want to have to do is put a wrapper around the result :
public class ItemsList
{
[JsonProperty("Items", ItemConverterType=typeof(StringEnumConverter))]
public List<Items> Items { get;set;
}
which, while it might technically work, would result in this endpoint being inconsistent with the rest of the API, which doesn't require wrappers round its results.
Try to add StringEnumConverter into your WebApiConfig
public static void Register(HttpConfiguration config)
{
config.Formatters.JsonFormatter.SerializerSettings.Converters.Add(new StringEnumConverter());
//...........................................
}
Related
I can't serialize tuples. I create a template VS2019 .Net Core API project and replace the controller with:
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
[HttpGet]
public List<(int number, string text)> Get()
{
var x = new List<(int number, string text)>
{
(1, "one"),
(2, "two"),
(3, "three")
};
return x;
}
/*[HttpGet]
public List<string> Get()
{
var x = new List<string>
{
"one",
"two",
"three"
};
return x;
}*/
}
When called, the first method will return: [{},{},{}]
and the second (when uncommented): ["one","two","three"]
Why aren't the tuples serialized?
The example is easy to replroduce.
Anonymous objects serialize better than value tuples, but declaring them is more verbose:
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
[HttpGet]
public IList<object> Get()
{
var x = new List<object>
{
new {number = 1, text = "one"},
new {number = 2, text = "two"},
new {number = 3, text = "three"}
};
return x;
}
}
I think this makes them clearer and more importantly it returns the expected: [{"number":1,"text":"one"},{"number":2,"text":"two"},{"number":3,"text":"three"}]
If you want to be really clear what your API methods are returning then I would declare DTO/model ojects to return.
I think most serialization libraries use public properties for output generation, and C# 7 tuples are using public fields instead. We can check this by doing some reflection. You can see that if you return object of your own class with public fields only, json response will have the same output. If it does not match your expectations, you could use different serialization strategy. Some handy answers are here
using Microsoft.AspNetCore.Mvc;
namespace WebApplication1.Controllers
{
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
[HttpGet]
public object Get()
{
var x = (1, 1);
var properties = x.GetType().GetProperties(); // count == 0
var fields = x.GetType().GetFields(); // count == 2
return x;
}
[HttpGet("obj")]
public object GetMyObj()
{
return new MyObj();
}
public class MyObj
{
public int i = 1;
}
}
}
You can return an OK response:
[HttpGet]
public IHttpActionResult Get()
{
var x = new List<(int number, string text)>
{
(1, "one"),
(2, "two"),
(3, "three")
};
return Ok(x);
}
You need use Newtonsoft.Json to serialize. Install the package from Nuget:
Install-Package Microsoft.AspNetCore.Mvc.NewtonsoftJson -Version 3.1.9
And use the below code to serilize it:
var y = JsonConvert.SerializeObject(x);
I'm using the [FromQuery] attribute to parse a Get requests arguments into a complex object. For example:
[HttpGet("some-get-request")]
public IActionResult DoSomething([FromQuery] SomeArguments someArgs)
{
}
One of the properties of the SomeArguments object is an enum.
public enum SomeEnum { EnumValue01, EnumValue02 }
public class SomeArguments
{
[FromQuery(Name = "enum_arg")]
public SomeEnum EnumArgument { get; set; }
}
And I call the endpoint with something like:
http://localhost:1234/api/some-controller/some-get-request?enum_arg=EnumValue01
And this all works great. However, I want to be able to use a different enum value in the URL than in my C# enum value. For example, I want to call using a URL such as
http://localhost:1234/api/some-controller/some-get-request?enum_arg=eval01
How can I do this?
I thought I could use the [FromQuery] attribute, like I can with properties, but that doesnt seem to be possible:
'FromQuery' is not valid on this declaration type. It is only valid on 'property, indexer, parameter'
You can use EnumMemberAttribute in conjunction with StringEnumConverter to achieve your goal. Define SomeEnum as following
[JsonConverter(typeof(StringEnumConverter))]
public enum SomeEnum
{
[EnumMember(Value = "eval01")]
EnumValue01,
[EnumMember(Value = "eval02")]
EnumValue02
}
At this point it will work as you need only when Newtonsoft json serializer is used. For example, when controller expects POST request and parameter is marked as [FromBody]. In your case it won't work yet because during binding of [FromQuery] parameter json serializer is not used. To solve this one create custom model binder
public class JsonModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
string rawData = bindingContext.ValueProvider.GetValue(bindingContext.ModelName).FirstValue;
rawData = JsonConvert.SerializeObject(rawData); //turns value to valid json
try
{
SomeEnum result = JsonConvert.DeserializeObject<SomeEnum>(rawData); //manually deserializing value
bindingContext.Result = ModelBindingResult.Success(result);
}
catch (JsonSerializationException ex)
{
//do nothing since "failed" result is set by default
}
return Task.CompletedTask;
}
}
Update SomeEnum definition to use JsonModelBinder
[JsonConverter(typeof(StringEnumConverter))]
[ModelBinder(typeof(JsonModelBinder))]
public enum SomeEnum
I am trying to send an array of integers to my action method the code looks like so:
[HttpGet]
public async Task<IActionResult> ServicesByCategoryIds([FromQuery] int[] ids)
{
var services = await _accountsUow.GetServiceProfilesByCategoryIdsAsync(ids);
return Ok(services);
}
I call the method like so: https://localhost:44343/api/accounts/servicesbycategoryids?ids=1&ids=2
but always get en empty array when I call this method even tho I pass the ids in the query string. I am using .net core 2.1.
everything I have googled suggests that this is in fact the way this is done. . .
is there something I am missing here?
Thank you!
Binding failed for Array parameter is a known issue under Asp.Net Core 2.1 which has been recorded Array or List in query string does not get parsed #7712.
For a tempory workaround, you could set the FromQuery Name Property like below:
[HttpGet()]
[Route("ServicesByCategoryIds")]
public async Task<IActionResult> ServicesByCategoryIds([FromQuery(Name = "ids")]int[] ids)
{
return Ok();
}
A slight variation on Plamen's answer.
Arrays seem to have an empty GenericTypeArguments so added GetElementType()
Renamed class to avoid clashing with the framework class ArrayModelBinder.
Added a check on the element type as it's required.
More options for surrounding the array with brackets.
public class CustomArrayModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (!bindingContext.ModelMetadata.IsEnumerableType)
{
bindingContext.Result = ModelBindingResult.Failed();
return Task.CompletedTask;
}
var value = bindingContext.ValueProvider
.GetValue(bindingContext.ModelName)
.ToString();
if (string.IsNullOrWhiteSpace(value))
{
bindingContext.Result = ModelBindingResult.Success(null);
return Task.CompletedTask;
}
var elementType = bindingContext.ModelType.GetElementType() ??
bindingContext.ModelType.GetTypeInfo().GenericTypeArguments.FirstOrDefault();
if (elementType == null)
{
bindingContext.Result = ModelBindingResult.Failed();
return Task.CompletedTask;
}
var converter = TypeDescriptor.GetConverter(elementType);
var values = value.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(x => converter.ConvertFromString(Clean(x)))
.ToArray();
var typedValues = Array.CreateInstance(elementType, values.Length);
values.CopyTo(typedValues, 0);
bindingContext.Model = typedValues;
bindingContext.Result = ModelBindingResult.Success(bindingContext.Model);
return Task.CompletedTask;
}
private static string Clean(string str)
{
return str.Trim('(', ')').Trim('[', ']').Trim();
}
}
Then use with an IEnumerable<T>, IList<T> or array T[]
[ModelBinder(BinderType = typeof(CustomArrayModelBinder))] IEnumerable<T> ids
... T[] ids
... IList<T> ids
The parameter could be in path or query with optional brackets.
[Route("resources/{ids}")]
resource/ids/1,2,3
resource/ids/(1,2,3)
resource/ids/[1,2,3]
[Route("resources")]
resource?ids=1,2,3
resource?ids=(1,2,3)
resource?ids=[1,2,3]
I create a new web api class, with only one action.
[Produces("application/json")]
[Route("api/accounts")]
public class AccountsController : Controller
{
[HttpGet]
[Route("servicesbycategoryids")]
public IActionResult ServicesByCategoryIds([FromQuery] int[] ids)
{
return Ok();
}
}
Then use the same url as yours:
http://localhost:2443/api/accounts/servicesbycategoryids?ids=1&ids=2
It is working.
You can implement custom model binder and the ids to be part of the URI, not in the query string.
Your endpoint could look like this:
/api/accounts/servicesbycategoryids/(1,2)
public class ArrayModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
// Our binder works only on enumerable types
if (!bindingContext.ModelMetadata.IsEnumerableType)
{
bindingContext.Result = ModelBindingResult.Failed();
return Task.CompletedTask;
}
// Get the inputted value through the value provider
var value = bindingContext.ValueProvider
.GetValue(bindingContext.ModelName).ToString();
// If that value is null or whitespace, we return null
if (string.IsNullOrWhiteSpace(value))
{
bindingContext.Result = ModelBindingResult.Success(null);
return Task.CompletedTask;
}
// The value isn't null or whitespace,
// and the type of the model is enumerable.
// Get the enumerable's type, and a converter
var elementType = bindingContext.ModelType.GetTypeInfo().GenericTypeArguments[0];
var converter = TypeDescriptor.GetConverter(elementType);
// Convert each item in the value list to the enumerable type
var values = value.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries)
.Select(x => converter.ConvertFromString(x.Trim()))
.ToArray();
// Create an array of that type, and set it as the Model value
var typedValues = Array.CreateInstance(elementType, values.Length);
values.CopyTo(typedValues, 0);
bindingContext.Model = typedValues;
// return a successful result, passing in the Model
bindingContext.Result = ModelBindingResult.Success(bindingContext.Model);
return Task.CompletedTask;
}
}
Then use it in your action:
[HttpGet("({ids})", Name="GetAuthorCollection")]
public IActionResult GetAuthorCollection(
[ModelBinder(BinderType = typeof(ArrayModelBinder))] IEnumerable<int> ids)
{
//enter code here
}
Learned this from a pluralsight course: Building RESTful API with ASP.NET Core
The answer is that simply decorating the array with the [FromQuery] attribute is all that's needed to make the binding work. Without that attribute it fails to bind. That's it, and #kennyzx's answer above is best, but I feel like the point needed to be as simply stated as this: [FromQuery] is all you need. I don't know why these other answers went the ModelBinder route, maybe that is needed for some scenarios, but in my case and I'm sure with many others, the key was to not forget to apply the [FromQuery] attribute.
public ActionResult GetFoo(int id, [FromQuery] Guid[] someIds) { ... }
Rather than [ab]using query string (consider 1000s of IDs), you can use [FromBody] instead, and pass list of IDs as a JSON array:
public IActionResult ServicesByCategoryIds([FromBody] int[] ids)
As long as OpenAPI/Swagger is concerned, a proper specification will be generated:
"parameters": [
{
"name": "ids",
"in": "body",
"required": true,
"schema": {
"type": "array",
"items": {
"type": "integer",
"format": "int32"
}
}
}
],
I am writing a REST interface using Jersey and have configured it to use JacksonXML to do the JSON<->POJO conversions. I have a set of classes like this:
#JsonInclude(JsonInclude.Include.NON_NULL)
public class JsonPo {
public Long id;
public String type;
public Boolean committed;
public String owner;
// ...
Which is referenced in my main class like this:
#JsonInclude(JsonInclude.Include.NON_NULL)
public class JsonPoReq {
public Long id;
public String pathstring;
public int firstrecord;
public JsonPo obj;
// ...
And the REST resource of course looks like this:
#POST
#Path("path")
#Consumes(MediaType.APPLICATION_JSON)
#Produces(MediaType.APPLICATION_JSON)
public Response byPath(JsonPoReq req) {
// ...
The serialization (POJO->JSON) works fine. The properties in the JsonPo object get serialized in a JSON object as you would expect.
The reverse doesn't work. Even if I feed the serialized string back. What happens is the obj property is always set to null.
EDIT: As requested the following is what the JSON string looks like:
{
"id" : 2,
"pathstring" : "/a/b/c",
"firstrecord" : 0,
"obj" : {
"id" : 2,
"type" : "ProtoObject",
"committed" : false,
"owner" : "admin"
}
}
I have been going through the JacksonXML Annotations to see how to fix this but I haven't been able to come up with anything. Is there some other step I am supposed to take?
UPDATE: Problem was not real. Some application code was being invoked that I didn't notice was clearing the object.
I am trying to use Ninject to implement cascading injection into a class that contains an IList field. It seems that, unless I specifically specify each binding to use in the kernel.Get method, the IList property is always injected with a list of a single default object.
The following VSTest code illustrates the problem. The first test fails because the IList field contains one MyType object with Name=null. The second test passes, but I had to specifically tell Ninject what constructor arguments to use. I am using the latest build from the ninject.web.mvc project for MVC 3.
Does Ninject specifically treat IList different, or is there a better way to handle this? Note that this seems to only be a problem when using an IList. Createing a custom collection object that wraps IList works as expected in the first test.
[TestClass()]
public class NinjectTest
{
[TestMethod()]
public void ListTest_Fails_NameNullAndCountIncorrect()
{
var kernel = new Ninject.StandardKernel(new MyNinjectModule());
var target = kernel.Get<MyModel>();
var actual = target.GetList();
// Fails. Returned value is set to a list of a single object equal to default(MyType)
Assert.AreEqual(2, actual.Count());
// Fails because MyType object is initialized with a null "Name" property
Assert.AreEqual("Fred", actual.First().Name);
}
[TestMethod()]
public void ListTest_Passes_SeemsLikeUnnecessaryConfiguration()
{
var kernel = new Ninject.StandardKernel(new MyNinjectModule());
var target = kernel.Get<MyModel>(new ConstructorArgument("myGenericObject", kernel.Get<IGenericObject<MyType>>(new ConstructorArgument("myList", kernel.Get<IList<MyType>>()))));
var actual = target.GetList();
Assert.AreEqual(2, actual.Count());
Assert.AreEqual("Fred", actual.First().Name);
}
}
public class MyNinjectModule : NinjectModule
{
public override void Load()
{
Bind<IList<MyType>>().ToConstant(new List<MyType> { new MyType { Name = "Fred" }, new MyType { Name = "Bob" } });
Bind<IGenericObject<MyType>>().To<StubObject<MyType>>();
}
}
public class MyModel
{
private IGenericObject<MyType> myGenericObject;
public MyModel(IGenericObject<MyType> myGenericObject)
{
this.myGenericObject = myGenericObject;
}
public IEnumerable<MyType> GetList()
{
return myGenericObject.GetList();
}
}
public interface IGenericObject<T>
{
IList<T> GetList();
}
public class StubObject<T> : IGenericObject<T>
{
private IList<T> _myList;
public StubObject(IList<T> myList)
{
_myList = myList;
}
public IList<T> GetList()
{
return _myList;
}
}
public class MyType
{
public String Name { get; set; }
}
lists, collections and arrays are handled slightly different. For those types ninject will inject a list or array containing an instance of all bindings for the generic type. In your case the implementation type is a class which is aoutobound by default. So the list will contain one instance of that class. If you add an interface to that class and use this one the list will be empty.