Change in behavior for controller parameter binding in NET7? - c#

I have an object as follows:
[Serializable]
[DataContract()]
public class MyObject
{
[DataMember(Order = 0)]
public int Id { get; set; }
[DataMember(Order = 1)]
public string Name { get; set; }
}
And I'm trying to post a list of objects to an API by doing:
public async void SaveAsync(IEnumerable<MyObject> items, CancellationToken ct = default)
{
var response = await client.PostAsJsonAsync(mySaveUrl, items, ct);
}
And the API endpoint is:
[ProducesResponseType(typeof(IEnumerable<DTO.MyObject>), StatusCodes.Status200OK)]
[HttpPost("SaveObjects")]
public async Task<ActionResult> SaveObjects(IEnumerable<DTO.MyObject> items)
{
await myService.SaveAsync(items);
return Ok();
}
However, when the endpoint is reached, the value of the items parameter is an empty array.
This was tried and tested code and was working for years and through many iterations of .NET.
To get the list to serialize successfully I now do need to add [FromBody]. I'm testing this directly with my WebAPI project.
What am I missing?
UPDATE
Is this a breaking change in NET7 as nowhere in my solution where I post lists of objects is working anymore. Which properties of the ApiBehaviorOptions class need to be set to get the same behavior as in NET6?
Here the list of breaking changes in NET7.

Related

Angular 8 post formData to ASP.NET Core API cannot bind IEnumerable/List

As title says
TypeScript model
export interface RigheOrdiniBuoniSpesaData {
id: number;
id_ordine: number;
id_taglio_buono_spesa: number;
quantita: number;
}
which is part of another bigger object:
export class OrdiniBuoniSpesaData {
id: number;
// OTHER FIELD
// OTHER FIELD
// OTHER FIELD
righe_ordine: RigheOrdiniBuoniSpesaTableData;
}
Save method
saveOrder(model: OrdiniBuoniSpesaData) {
const headerPost: HttpHeaders = new HttpHeaders();
headerPost.set('Content-type', 'application/x-www-form-urlencoded');
const formData: FormData = new FormData();
formData.append('id_cliente', model.id_cliente.toString());
// VARIOUS FORM FIELDS
//THIS IS ARRAY DATA
formData.append('righe_ordine', JSON.stringify(model.righe_ordine));
return this.http
.post<boolean>(
requestURL,
formData,
{ headers: headerPost }
)
.pipe(
catchError(this.handleError)
);
}
Order json (valid Json) is visible in Chrome request capture clearly along with all data:
[{"id":0,"id_ordine":0,"id_taglio_buono_spesa":1,"quantita":1},{"id":0,"id_ordine":0,"id_taglio_buono_spesa":1,"quantita":1},{"id":0,"id_ordine":0,"id_taglio_buono_spesa":1,"quantita":1},{"id":0,"id_ordine":0,"id_taglio_buono_spesa":3,"quantita":14}]
On API Side
Receiving model for JSON
public class RigheOrdiniBuoniSpesaViewModel
{
public long id { get; set; }
public long id_ordine { get; set; }
public long id_taglio_buono_spesa { get; set; }
public int quantita { get; set; }
}
Which is in
public class OrdiniBuoniSpesaViewModel
{
public long id { get; set; }
//OTHER VARIOUS FIELDS
//I TRIED ALSO WITH LIST INSTEAD OF IENUMERABLE
public IEnumerable<RigheOrdiniBuoniSpesaViewModel> righe_ordine {get;set;}
}
(I TRIED ALSO WITH LIST INSTEAD OF IENUMERABLE, STILL NO LUCK!)
Api controller signature:
[HttpPost]
public async Task<IActionResult> PostSaveOrder([FromForm] OrdiniBuoniSpesaViewModel model)
{.....code}
All the fields are binded correctly except for the righe_ordine array!
I can see every field correctly but the array has count = 0.
Strangely enough, if I examine the asp net request object (this.Request.Form) in the QuickWatch debug in visual studio:
this.Request.Form["righe_ordine"]
{[{"id":0,"id_ordine":0,"id_taglio_buono_spesa":1,"quantita":1},
{"id":0,"id_ordine":0,"id_taglio_buono_spesa":1,"quantita":1},
{"id":0,"id_ordine":0,"id_taglio_buono_spesa":1,"quantita":1},
{"id":0,"id_ordine":0,"id_taglio_buono_spesa":3,"quantita":14}]}
Microsoft.Extensions.Primitives.StringValues
is present and correctly populated..... but for some reason binding it to the OrdiniBuoniSpesaViewModel fails....
What am I doing wrong? Any idea?
EDIT:
For the moment the only solution I found is to directly catch the value just after entering the controller:
string righeJson = this.Request.Form["righe_ordine"].ToString();
if(!string.IsNullOrEmpty(righeJson))
model.righe_ordine = JsonConvert.DeserializeObject<IEnumerable<RigheOrdiniBuoniSpesaViewModel>>(righeJson);
I think you need to change your model name like this
In your js code
formData.append('righeOrdine', JSON.stringify(model.righe_ordine));
and c#
public class OrdiniBuoniSpesaViewModel
{
public long id { get; set; }
//OTHER VARIOUS FIELDS
//I TRIED ALSO WITH LIST INSTEAD OF IENUMERABLE
public IEnumerable<RigheOrdiniBuoniSpesaViewModel> RigheOrdine {get;set;}
}
Update: Just for enhancement
Maybe you need to parse it before using it in your controller
You can read my full code here
We will need to create interface and class to implement it.
public interface IJsonParseService<T> where T : class
{
T ToObject(string json);
}
public class JsonParseService<T> : IJsonParseService<T> where T : class
{
public T ToObject(string json)
{
return JObject.Parse(json).Root.ToObject<T>();
}
}
Then register it
services.AddScoped(typeof(IJsonParseService<>), typeof(JsonParseService<>));
So your code should be something like this
private readonly IJsonParseService<RigheOrdiniBuoniSpesaViewModel> _jsonParsePostOptionDefaultVm;
jsonParsePostOptionDefaultVm.ToObject(viewModel.RigheOrdiniBuoniSpesaViewModel);

How to pass multiple parameters to Get Method and Route them in .NET Core Web API?

I'm making a (restful) Web API in .NET Core and stumbled among some problems.
I cannot seem to find how to pass multiple subscription ID's... I need to be able to show multiple periods(invoices) of multiple subscriptions.
My route at the moment is
[Route("tenants/{tenantId:long}/subscriptions/{subscriptionId:long}/invoices/{invoiceId:long}/categories")]
From this way it seems impossible for me to pass more subscription IDs.
Some terms I found but not fully understand are:
Model Binding
[FromQuery]
My classes:
public class Subscription
{
public string Name { get; set; }
public long TenantId { get; set; }
public string Guid { get; set; }
}
public class Invoice
{
public long SubscriptionId { get; set; }
public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; }
public long PortalId { get; set; }
}
My controllers with routes [Route("tenants/{tenantId:long}/subscriptions")] and [Route("tenants/{tenantId:long}/subscriptions/{subscriptionId:long}/invoices")]
[HttpGet]
public IEnumerable<SubscriptionViewModel> Find(long tenantId)
{
var subscriptionList = _subscriptionManager.Find(tenantId);
...
return subscriptionViewModels;
}
[HttpGet]
public IEnumerable<InvoiceViewModel> Find(long subscriptionId)
{
var invoiceList = _invoiceManager.Find(subscriptionId);
...
return invoiceViewModels;
}
Please note that i'm using a Mapper for my data (which is why i'm using ViewModels).
The currently written code is for a specific subscription.
I am looking for a Route like /api/invoices?subscriptionId=x,y,z
I understand(?) I need the [FromQuery] for that, but I cannot seem to find out how, especially if my parameter (subscriptionId) stays the same.
for the requirement which you have mentioned as:
I am looking for a Route like /api/invoices?subscriptionId=x,y,z
You can do couple of things:
pass the subscriptionIds one after the other separated by & in the query string of the URL and change the input parameter of action method to accept array of subscriptionIds
example of route:
/api/invoices/find?subscriptionId=x&subscriptionId=y&subscriptionId=z
example of action method parameter accepting array of subscriptionIds:
public IEnumerable<InvoiceViewModel> Find([FromQuery]long[] subscriptionId)
pass the comma separated string as querystring in the URL and write a piece of logic in the action method to split the string based on comma to get an array of subscriptionIds
example of route:
/api/invoices/find?subscriptionIds=x,y,z
example of action method:
public IEnumerable<InvoiceViewModel> Find([FromQuery]string subscriptionIds)
{
var ids = subscriptionIds.Split(',').Select(int.Parse).ToArray();
// do the logic on multiple subscriptionIds
}
Apart from this, you can go for creating custom model binders as well as suggested in other answers.
Hope this helps.
There can be many ways to achieve this task (I can think of two-three for now).
1) instead of long subscriptionid take a string as an input and validate it before proceeding further.
[HttpGet]
public IEnumerable<InvoiceViewModel> Find(string subscriptionIds)
{
var list = validateInput(subscriptionIds);
var invoiceList = _invoiceManager.FindList(list);
...
return invoiceViewModels;
}
public IList<long> validateInput(string subscriptionIds)
{
var list = subscriptionIds.Split(",");
... // Code to convert each element in long and throw if it is not long
return longlist;
}
2) Create custom model binders.
Steps are mentioned here:
https://learn.microsoft.com/en-us/aspnet/web-api/overview/formats-and-model-binding/parameter-binding-in-aspnet-web-api
=> [FromUri] attribute can be used to bind the Complex types from query string parameters but i am not sure how i would use that.
If you ask me, i would go for approach-1 (not to increase complexity).
You can create a specific Request view model which accepts a collection of invoice ids:
public class InvoiceRequestModel
{
IEnumerable<long> InvoiceIDS { get; set; }
}
and use it for your action method:
[Route("tenants/{tenantId:long}/subscriptions/{subscriptionId:long}/invoices")]
[HttpGet]
public IEnumerable<InvoiceViewModel> Get(InvoiceRequestModel requestModel)
{
}
In the case you want to use query parameters, mark your action parameter with the [FromQuery] attribute:
[Route("tenants/{tenantId:long}/subscriptions/{subscriptionId:long}/invoices")]
[HttpGet]
public IEnumerable<InvoiceViewModel> Get([FromQuery]IEnumerable<long> invoiceIDs)
{
}
and on creating the request, pass each value with the same key in the query string:
invoiceIDs=1&invoiceIDs=2&invoiceIDs=3
Finally, it will look like this:
tenants/{tenantId}/subscriptions/{subscriptionId}/invoices?invoiceIDs=1&invoiceIDs=2&invoiceIDs=3

Why is my response being serialized without navigation properties?

Today I ran into a small problem with my code. I have an HttpGet method similar to the following:
[HttpGet]
public IEnumerable<SomeEntity> Get()
{
return db.SomeEntity.ToList();
}
where SomeEntity could be represented as
public class SomeEntity
{
#region DatabaseColumns
[Key]
public int SomeEntityID { get; set; }
public string SomeEntityName { get; set; }
#endregion
#region Navigation Properties
public virtual ICollection<SomeChildEntity> SomeChildEntity { get; set; }
#endregion
}
I noticed that return db.SomeEntity.ToList(); only returned the top level members of the object (not the navigation properties).
This made sense to me considering I was not calling Include. Out of curiosity, I attempted the following:
[HttpGet]
public IEnumerable<SomeEntity> Get()
{
var enumeratedEntity = db.SomeEntity.ToList();
return enumeratedEntity;
}
To my surprise, it returned the entire entity along with its navigation properties.
I also noticed that an HttpGet along these lines also returned the entire object
[HttpGet]
public SomeEntity Get(int id)
{
return db.SomeEntity.Find(id);
}
Can someone please explain, or point me to resources that will explain, why these methods return the entire object without using the Includes method while the first one doesn't?
I'm not 100% on this, so feel free to test and then up or downvote me. When you return an IEnumerable<SomeEntity> you don't define a concrete type. You're returning some generic IEnumerable which the HTTP pipeline then strips the virtual properties out of.
When you call var enumeratedEntity = db.SomeEntity.ToList(); you create a List. That concrete object then gets the virtual properties immediately instantiated. The whole concrete object is then sent down the pipeline including the virtual properties.
You could test this by changing var enumeratedEntity = db.SomeEntity.ToList(); to IEnumerable<SomeEntity> enumeratedEntity = db.SomeEntity.ToList(); and List<SomeEntity> enumeratedEntity = db.SomeEntity.ToList();. You'd then expect to see the 2 behaviors you currently see based on which generic container you use.

OWIN ApiController access to the request body/stringified JSON

This is in OWIN & .Net 4.5.2
Using debug I'm proving this controller's method is being called by the web request.
My thing is the request body contains a JSON stringified object:
"{ 'id':'12', 'text1':'hello', 'test2':'world' }"
Which is applicably encoded as it is transferred on the line.
I've tried so many things I'm so confused now.
How do I get the decoded string so I can JSON.Parse() that or better yet get .Net to just given me an object?
In one version (long ago now) I had a defined type for this object. If I need that great, not a high challenge. But if I only have the JSON object from the string that's fine too.
public class cController : ApiController {
[HttpPut]
public string put(string id) {
var bdy = this.Request.Content;
//Console.WriteLine("PUT containers {0}", body);
return string.Empty;
}
}
In case it helps, the bdy.ContentReadStream is null. I don't know if this is good, bad, or important. Maybe Request.Content isn't the way to go but seems to me like if I'm going to read the body as a stream then it shouldn't be null.
I also tried working through System.Web.HttpContext. If that is somehow the answer I have no problem going back to that. But I couldn't find the secret sauce.
Pass the desired model as a parameter to the action and the frame work should be able to parse it provided it is valid JSON
public class cController : ApiController {
[HttpPut]
public IHttpActionResult Put(string id,[FromBody] Model body) {
if(ModelState.IsValue) {
return Ok(body.text1);
}
return BadRequest();
}
}
Where Model is defined as
public class Model {
public string id { get; set; }
public string text1 { get; set; }
public string test2 { get; set; }
}

ASP.NET Web API model binding non-sequential list of complex objects

I am attempting to model bind a complex object with a non-sequential list using an ApiController. All of the fields except the list are set correctly, but the list contains one element (even though two list elements were posted) and the element is null. If I take the exact same code and point it to an MVC Controller using the same parameter type in my action method, everything works as expected.
Since I am using a non-sequential list, I am using the hidden ".Index" input as described by Phil Haack (http://haacked.com/archive/2008/10/23/model-binding-to-a-list.aspx)
The ApiController also binds the list correctly if I remove the ".Index" input and send the list as a sequential list starting at 0. (This option work for testing, but is not a great option in production as the list items can be added and removed by the user, which is why I want to use the non-sequential list.)
I understand that Web API Controllers do parameter binding differently than MVC Controllers as discussed here, but it seems like non-sequential lists should bind correctly in Web API Controllers. Am I missing something? Why does the same code work for an MVC Controller and not a Web API Controller? How can I get non-sequential lists to bind correctly in Web API?
Here are my Post parameters:
Parameters application/x-www-form-urlencoded
BatchProductLots.Index 1
BatchProductLots.Index 2
BatchProductLots[1].BrandId 1
BatchProductLots[1].ContainerId 9
BatchProductLots[1].ContainerLot 123
BatchProductLots[1].PackageId 2
BatchProductLots[1].PlannedQuantity 0
BatchProductLots[1].ProducedQuantity 20
BatchProductLots[2].BrandId 1
BatchProductLots[2].ContainerId 9
BatchProductLots[2].ContainerLot 123
BatchProductLots[2].PackageId 1
BatchProductLots[2].PlannedQuantity 0
BatchProductLots[2].ProducedQuantity 1
BatchStatusId 1
LotNumber 070313
ProductionDate 07/03/2013
RecipeId 1
RecipeQuantity 1
SauceId 22
X-Requested-With XMLHttpRequest
Here is my Web API Controller Action:
(request.BatchProductLots list is set to one element (even though two elements were posted) and that one element is null)
public Response Create(BatchCreateRequest request)
{
Response response = new Response();
try
{
Batch batch = Mapper.Map<Batch>(request);
batchService.Save(batch);
response.Success = true;
}
catch (Exception ex)
{
response.Message = ex.Message;
response.Success = false;
}
return response;
}
Here is the complex object with the list that I am attempting to bind to:
public class BatchCreateRequest
{
public int BatchStatusId { get; set; }
public DateTime ProductionDate { get; set; }
public string LotNumber { get; set; }
public int SauceId { get; set; }
public int RecipeId { get; set; }
public int RecipeQuantity { get; set; }
public List<BatchProductLot> BatchProductLots { get; set; }
public class BatchProductLot
{
public int BrandId { get; set; }
public int ContainerId { get; set; }
public string ContainerLot { get; set; }
public int PackageId { get; set; }
public int PlannedQuantity { get; set; }
public int ProducedQuantity { get; set; }
}
}
Short answer, it's not possible using Web Api's Model Binder. MVC and Web Api use different model binders and the Web Api model binder only works on simple types.
See this answer for links that explain further as well as possible solutions.
Longer answer, create a custom implementation of System.Web.Http.ModelBinding.IModelBinder and change your Action's signature to the following
public Response Create([ModelBinder(CustomModelBinder)]BatchCreateRequest request)
Do you really need Index to be set? In that case, one possible solution could be to make Index part of the BatchProductLot class. The sequence of list won't matter then and Web Api should be able to bind it.
Another idea would be to use application/json content type and send JSON. You can use Json.Net to deserialize and model binding would work.
Read Using an alternate JSON Serializer in ASP.NET Web API and even use this Nuget Package WebApi Json.NET MediaTypeFormatter if you don't want to do the hand wiring.

Categories

Resources