ASP.NET Core 5 MVC : post model binding complex collection back to controller instantiates vm but without data - collections

I'm trying to post back a complex collection (POCOs) to my controller. The array created by serializeArray() looks good to me (but maybe it is not).
I tried conforming my code in this project to code that works in an ASP.NET Core 5 MVC & EF 6 project I have. Something has changed since then. Can't figure out what I am missing.
Here's my current version of the Ajax call:
$('#btnCreate').click(function(e) {
e.preventDefault();
$.ajax({
type: 'POST',
url: '#Url.Action("CreatePL", "Reports")',
data: JSON.stringify({ glassDemands: $('#plTarget').serializeArray() }),
contentType: 'application/json; charset=UTF-8',
success: function(data) {
},
error: function(xhr) {
}
});
});
Here's a screenshot of the first several lines of data it creates:
This screenshot shows the action in the controller with result from post:
Here are the three classes that make up the view. Microsoft documentation states that
POCOs that do not have parameterless constructors can't be bound.
Code:
public class GlassDemand
{
public GlassDemand()
{
Boxes = new HashSet<Box>();
}
public string bunk { get; set; }
public int sequence { get; set; }
public string tag { get; set; }
public string elevation { get; set; }
[Display(Name = "Glass")]
public string glasstag { get; set; }
[Display(Name = "Needed")]
public int glassqty { get; set; }
// total quantity selected from boxes. should be the sum of all related boxitem.usedqty
[NotMapped]
[Display(Name = "Sel Qty")]
public int pickedqty { get; set; }
[NotMapped]
public ICollection<Box> Boxes { get; set; }
[NotMapped]
public int ItemId { get; set; }
}
public class Box
{
public int BoxId { get; set; }
public string Name { get; set; }
public bool Selected { get; set; }
public string Location { get; set; }
public virtual ICollection<BoxItem> BoxItems { get; set; }
public int MatchedTo { get; set; }
public Box()
{
BoxItems = new HashSet<BoxItem>();
}
}
public class BoxItem
{
public BoxItem()
{
}
public int ItemId { get; set; }
public string Name { get; set; }
[DisplayFormat(DataFormatString = "{0:N0}")]
public decimal Qty { get; set; }
public int BoxId { get; set; }
public int UsedById { get; set; }
[DisplayFormat(DataFormatString = "{0:N0}")]
public decimal UsedQty { get; set; }
// remaining = Qty (box quantity) - sum(UsedQty from all instances)
[DisplayFormat(DataFormatString = "{0:N0}" )]
public decimal Remaining { get; set; }
// navigations
public virtual Box Box { get; set; }
}
And the view model that is sent to the view when it is launched.
public class PickListDemandVM
{
public IEnumerable<GlassDemand> GlassDemands { get; set; }
public PickListDemandVM()
{
GlassDemands = new List<GlassDemand>();
}
}

.serializeArray() is used to post application/x-www-url-formencoded content type data to backend.Change your code like below:
$('#btnCreate').click(function (e) {
e.preventDefault();
$.ajax({
type: 'POST',
url: '#Url.Action("CreatePL", "Reports")',
data: $('#plTarget').serializeArray() ,
//contentType: 'application/json; charset=UTF-8',
success: function(data) {
},
error: function(xhr) {
}
});
});
Remove [FromBody] in Controller:
[HttpPost]
public ActionResult CreatePL(List<GlassDemand> glassDemands)

I don't understand all the nuances of model bindings. One thing is for sure though, you have to have keys on all tables being posted. With proper keys in place, I can use a view model or a List<> parameter in my controller action. From MS documentation, using a view model is the preferred method.
My POCOs didn't have keys. I ended up using a counter while I built my data on the highest level table, GlassDemand. Then built increasingly more complex keys for each descendent.
I added the following to my classes to hold the values that are dynamically being created for their respective tables.
[Key]
public string Id { get; set; }
I added this line to my display templates
#Html.HiddenFor(model => model.Id)
This is the first two rows look like that is being posted back to the controller now.
My dataset was also too large. I had to add the following attribute to the controller action. Thanks to DAustin's post
[RequestFormLimits(ValueCountLimit = 10000)]
the final jquery that posts back the data:
$('#btnCreate').click(function(e) {
e.preventDefault();
$.ajax({
type: 'POST',
url: '#Url.Action("CreatePL", "Reports")',
data: $('#plTarget').serializeArray(),
success: function(data) {
},
error: function(xhr) {
}
});
});
the final action in the controller:
[HttpPost]
[RequestFormLimits(ValueCountLimit = 10000)]
public ActionResult CreatePL(PickListDemandVM vm)
{

Related

ASP.NET Web API with Many-Many relationships

I'm having some trouble with ASP.NET Web API with many-many relationships between models. Here are my models (which I've simplified for brevity):
public class Model1
{
public int Model1ID { get; set; }
public string Name { get; set; }
public virtual ICollection<Model2> Model2s{ get; set; }
public string Self
{
get
{
return string.Format(CultureInfo.CurrentCulture,
"api/model1/{0}", this.Model1ID);
}
set { }
}
}
public class Model2
{
public int Model2ID { get; set; }
public string Name { get; set; }
public virtual ICollection<Model1> Model1s{ get; set; }
public string Self
{
get
{
return string.Format(CultureInfo.CurrentCulture,
"api/model2/{0}", this.Model2ID);
}
set { }
}
}
and my relevant Model1 API controller excerpt:
public class Model1sController : ApiController
{
private ApplicationDbContext db = new ApplicationDbContext();
// GET: api/Model1s
public IQueryable<Model1> GetModel1s()
{
return db.Model1s;
}
...
}
When I navigate to /api/model1s I get a long JSON nested error, here is the innermost Exception message.
There is already an open DataReader associated with this Command which must be closed first.
What I'm trying to achieve is output like this, but I cannot figure out how to get it working.
[{
"Model1ID": 1,
"Name": "Some model 2 name",
"Model2s": [{
"Model2ID": 1,
"Name": "Another model 2 name"
}, {
"Model2ID": 2,
"Name": "Some model 2 name"
}]
}, {
"Model1ID": 2,
"Name": "Another model 1 name",
"Model2s": [{
"Model2ID": 2,
"Name": "Some model 2 name"
}]
}]
What you need is called and associative entity, some devs call them a lookup table. An associative entity will hold the “association” between two other entities. In your case I believe that there is a scaffolding engine that will build the database tables for you based on the classes you create. Someone else may be able to speak to how the scaffolding engine works.
I would create a class called “TvProgramming” and give it properties Name, Id, Host_Id, Host_Name, and List. Now with this set up you can have as many hosts and as many tv shows as you want and still create unique programming schedules.
Adjust the tv show and host objects so that they only have properties that are unique to themselves ie a TvShow will have an name, id, and maybe a length. A host may have name, id, network, and location info however notice that the host object and tv show object have no knowledge of the other, only the associative entity holds knowledge of the relationship between them.
At the end of the day what your api should return is a set of TvProgramming objects that contain the hosts and for each host a list of tv shows… here is an quick example of the class structure I’m talking about, you’ll have to tweak it a bit to fit your needs but it should get started.
namespace YourProjectName.Models
{
public class TvShow
{
public int id { get; set; }
public string name { get; set; }
public TimeSpan length { get; set; }
public string rating { }
public TvShow() { }
}
public class Host
{
public int id { get; set; }
public string name { get; set; }
public string network { get; set; }
public string city { get; set; }
public string state { get; set; }
public string zip { get; set; }
public string country { get; set; }
public Host() { }
}
public class TvProgramming
{
public int id { get; set; }
public string name { get; set; }
public int host_Id { get; set; }
public string host_Name { get; set; }
public List<TvShow> shows { get; set; }
public TvProgramming()
{
this.shows = new List<TvShow>();
}
}
}
As a way of possibly preventing the error you are getting, try modifying your Controller code like this:
public class Model1sController : ApiController
{
// GET: api/Model1s
public IQueryable<Model1> GetModel1s()
{
using (var db = new ApplicationDbContext())
{
return db.Model1s;
}
}
}

Entity Framework Core Query Specific Model both directions

Let me preface this question with, I am VERY new to ASP.NET Core/EF Core.
My model look like this:
namespace MyProject.Models
{
public class DeviceContext : DbContext
{
public DeviceContext(DbContextOptions<DeviceContext> options) : base(options) { }
public DbSet<Device> Devices { get; set; }
public DbSet<DeviceLocation> DeviceLocations { get; set; }
}
public class Device
{
public int Id { get; set; }
public string DeviceName { get; set; }
public string ServerName { get; set; }
public string MacAddress { get; set; }
public string LastUpdate { get; set; }
public string WiredIPAddress { get; set; }
public string WirelessIPAddress { get; set; }
public DeviceLocation DeviceLocation { get; set; }
}
public class DeviceLocation
{
public int Id { get; set; }
public string Location { get; set; }
public virtual ICollection<Device> Devices { get; set; }
}
}
I would like to be able to fetch a specific Device based on DeviceName, but I would also like to fetch ALL the devices in a particular Location.
I think the following would work for the first question:
var _Devices = DeviceContext.Devices.FirstOrDefault(d => d.DeviceName == "BLA");
I am just having a hard time getting the second query to run. Ideally, the output would be rendered to JSON to be consumed by an API. I would like the output to look something like this:
{
"Locations": {
"NYC": ["ABC", "123"],
"Boston": ["DEF", "456"],
"Chicago": ["GHI", "789"]
}
}
UPDATE
If I use the following code, it give me the following error:
Code:
// Grouping by ProfileName
var devices = DeviceContext.DeviceLocations.Include(n => n.Device).ToList();
var result = new { success = true, message = "Successfully fetched Devices", data = devices };
return JsonConvert.SerializeObject(result);
Error:
Additional information: Self referencing loop detected for property 'DeviceLocation' with type 'Project.Models.DeviceLocation'. Path 'data[0].Device[0]'.
You can try as shown below.
Note : Use Eager Loading with Include.
using System.Data.Entity;
var devicesList = DeviceContext.DeviceLocations.Where(d=>d.Location = "Your-Location-Name")
.Include(p => p.Devices)
.ToList();
Update :
var devicesList = DeviceContext.DeviceLocations
.Include(p => p.Devices)
.ToList();

Using DTO's with OData & Web API

Using Web API and OData, I have a service which exposes Data Transfer Objects instead of the Entity Framework entities.
I use AutoMapper to transform the EF Entities into their DTO counter parts using ProjectTo():
public class SalesOrdersController : ODataController
{
private DbContext _DbContext;
public SalesOrdersController(DbContext context)
{
_DbContext = context;
}
[EnableQuery]
public IQueryable<SalesOrderDto> Get(ODataQueryOptions<SalesOrderDto> queryOptions)
{
return _DbContext.SalesOrders.ProjectTo<SalesOrderDto>(AutoMapperConfig.Config);
}
[EnableQuery]
public IQueryable<SalesOrderDto> Get([FromODataUri] string key, ODataQueryOptions<SalesOrderDto> queryOptions)
{
return _DbContext.SalesOrders.Where(so => so.SalesOrderNumber == key)
.ProjectTo<SalesOrderDto>(AutoMapperConfig.Config);
}
}
AutoMapper (V4.2.1) is configured as follows, note the ExplicitExpansion() which prevents serialisation auto expanding navigation properties when they are not requested:
cfg.CreateMap<SalesOrderHeader, SalesOrderDto>()
.ForMember(dest => dest.SalesOrderLines, opt => opt.ExplicitExpansion());
cfg.CreateMap<SalesOrderLine, SalesOrderLineDto>()
.ForMember(dest => dest.MasterStockRecord, opt => opt.ExplicitExpansion())
.ForMember(dest => dest.SalesOrderHeader, opt => opt.ExplicitExpansion());
ExplicitExpansion() then creates a new problem where the following request throws an error:
/odatademo/SalesOrders('123456')?$expand=SalesOrderLines
The query specified in the URI is not valid. The specified type member 'SalesOrderLines' is not supported in LINQ to Entities
The navigation property SalesOrderLines is unknown to EF so this error is pretty much what I expected to happen. The question is, how do I handle this type of request?
The ProjectTo() method does have an overload that allows me to pass in an array of properties that require expansion, I found & modified the extension method ToNavigationPropertyArray to try and parse the request into a string array:
[EnableQuery]
public IQueryable<SalesOrderDto> Get([FromODataUri] string key, ODataQueryOptions<SalesOrderDto> queryOptions)
{
return _DbContext.SalesOrders.Where(so => so.SalesOrderNumber == key)
.ProjectTo<SalesOrderDto>(AutoMapperConfig.Config, null, queryOptions.ToNavigationPropertyArray());
}
public static string[] ToNavigationPropertyArray(this ODataQueryOptions source)
{
if (source == null) { return new string[]{}; }
var expandProperties = string.IsNullOrWhiteSpace(source.SelectExpand?.RawExpand) ? new List<string>().ToArray() : source.SelectExpand.RawExpand.Split(',');
for (var expandIndex = 0; expandIndex < expandProperties.Length; expandIndex++)
{
// Need to transform the odata syntax for expanding properties to something EF will understand:
// OData may pass something in this form: "SalesOrderLines($expand=MasterStockRecord)";
// But EF wants it like this: "SalesOrderLines.MasterStockRecord";
expandProperties[expandIndex] = expandProperties[expandIndex].Replace(" ", "");
expandProperties[expandIndex] = expandProperties[expandIndex].Replace("($expand=", ".");
expandProperties[expandIndex] = expandProperties[expandIndex].Replace(")", "");
}
var selectProperties = source.SelectExpand == null || string.IsNullOrWhiteSpace(source.SelectExpand.RawSelect) ? new List<string>().ToArray() : source.SelectExpand.RawSelect.Split(',');
//Now do the same for Select (incomplete)
var propertiesToExpand = expandProperties.Union(selectProperties).ToArray();
return propertiesToExpand;
}
This works for expand, so now I can handle a request like the following:
/odatademo/SalesOrders('123456')?$expand=SalesOrderLines
or a more complicated request like:
/odatademo/SalesOrders('123456')?$expand=SalesOrderLines($expand=MasterStockRecord)
However, more complicated request that try to combine $select with $expand will fail:
/odatademo/SalesOrders('123456')?$expand=SalesOrderLines($select=OrderQuantity)
Sequence contains no elements
So, the question is: am I approaching this the right way?
It feels very smelly that I would have to write something to parse and transform the ODataQueryOptions into something EF can understand.
It seems this is a rather popular topic:
odata-expand-dtos-and-entity-framework
how-to-specify-the-shape-of-results-with-webapi2-odata-with-expand
web-api-queryable-how-to-apply-automapper
how-do-i-map-an-odata-query-against-a-dto-to-another-entity
While most of these suggest using ProjectTo, none seem to address serialisation auto expanding properties, or how to handle expansion if ExplictExpansion has been configured.
Classes and Config below:
Entity Framework (V6.1.3) entities:
public class SalesOrderHeader
{
public string SalesOrderNumber { get; set; }
public string Alpha { get; set; }
public string Customer { get; set; }
public string Status { get; set; }
public virtual ICollection<SalesOrderLine> SalesOrderLines { get; set; }
}
public class SalesOrderLine
{
public string SalesOrderNumber { get; set; }
public string OrderLineNumber { get; set; }
public string Product { get; set; }
public string Description { get; set; }
public decimal OrderQuantity { get; set; }
public virtual SalesOrderHeader SalesOrderHeader { get; set; }
public virtual MasterStockRecord MasterStockRecord { get; set; }
}
public class MasterStockRecord
{
public string ProductCode { get; set; }
public string Description { get; set; }
public decimal Quantity { get; set; }
}
OData (V6.13.0) Data Transfer Objects:
public class SalesOrderDto
{
[Key]
public string SalesOrderNumber { get; set; }
public string Customer { get; set; }
public string Status { get; set; }
public virtual ICollection<SalesOrderLineDto> SalesOrderLines { get; set; }
}
public class SalesOrderLineDto
{
[Key]
[ForeignKey("SalesOrderHeader")]
public string SalesOrderNumber { get; set; }
[Key]
public string OrderLineNumber { get; set; }
public string LineType { get; set; }
public string Product { get; set; }
public string Description { get; set; }
public decimal OrderQuantity { get; set; }
public virtual SalesOrderDto SalesOrderHeader { get; set; }
public virtual StockDto MasterStockRecord { get; set; }
}
public class StockDto
{
[Key]
public string StockCode { get; set; }
public string Description { get; set; }
public decimal Quantity { get; set; }
}
OData Config:
var builder = new ODataConventionModelBuilder();
builder.EntitySet<StockDto>("Stock");
builder.EntitySet<SalesOrderDto>("SalesOrders");
builder.EntitySet<SalesOrderLineDto>("SalesOrderLines");
I have created an Automapper explicit navigation expansion utility function that should work with N-deph expands. Posting it here since it might help someone.
public List<string> ProcessExpands(IEnumerable<SelectItem> items, string parentNavPath="")
{
var expandedPropsList = new List<String>();
if (items == null) return expandedPropsList;
foreach (var selectItem in items)
{
if (selectItem is ExpandedNavigationSelectItem)
{
var expandItem = selectItem as ExpandedNavigationSelectItem;
var navProperty = expandItem.PathToNavigationProperty?.FirstSegment?.Identifier;
expandedPropsList.Add($"{parentNavPath}{navProperty}");
//go recursively to subproperties
var subExpandList = ProcessExpands(expandItem?.SelectAndExpand?.SelectedItems, $"{parentNavPath}{navProperty}.");
expandedPropsList = expandedPropsList.Concat(subExpandList).ToList();
}
}
return expandedPropsList;
}
You can call it with :
var navExp = ProcessExpands(options?.SelectExpand?.SelectExpandClause?.SelectedItems)
it will return a list with ["Parent" ,"Parent.Child"]
I never really managed to work this one out. The ToNavigationPropertyArray() extension method helps a little, but does not handle infinite depth navigation.
The real solution is to create Actions or Functions to allow clients to request data requiring a more complicated query.
The other alternative is to make multiple smaller/simple calls then aggregate the data on the client, but this isn't really ideal.
When you want to mark something for explicit expansion in AutoMapper, you need to also opt-back-in when calling ProjectTo<>().
// map
cfg.CreateMap<SalesOrderHeader, SalesOrderDto>()
.ForMember(dest => dest.SalesOrderLines, opt => opt.ExplicitExpansion());
// updated controller
[EnableQuery]
public IQueryable<SalesOrderDto> Get()
{
return _dbContext.SalesOrders
.ProjectTo<SalesOrderDto>(
AutoMapperConfig.Config,
so => so.SalesOrderLines,
// ... additional opt-ins
);
}
While the AutoMapper wiki does state this, the example is perhaps a little misleading by not including the paired ExplicitExpansion() call.
To control which members are expanded during projection, set ExplicitExpansion in the configuration and then pass in the members you want to explicitly expand:

How do I change this controller to take multiple records, usinv the ASP.Net MVC Web API?

I have a booking app, which accepts a single "booking" object, and works ok. My question is, how do I convert this to accept multiple records (from JSON):
Booking.cs
namespace MvcApplication4.Models
{
public class Booking
{
[Key()]
public long ID { get; set; }
public long? HID { get; set; }
public long RID { get; set; }
public string Occ { get; set; }
public DateTime CI { get; set; }
public DateTime CO { get; set; }
public long? CID { get; set; }
}
}
BookingsContext.cs
namespace MvcApplication4.Models
{
public class BookingsContext : DbContext
{
public BookingsContext() : base("name=BookingsContext")
{
}
public DbSet<Booking> Bookings { get; set; }
}
}
BookingsController.cs
// POST api/Bookings
public HttpResponseMessage PostBooking(Booking booking)
{
if (ModelState.IsValid)
{
// Add the booking
db.Bookings.Add(booking);
db.SaveChanges();
HttpResponseMessage response = Request.CreateResponse(HttpStatusCode.Created, booking);
response.Headers.Location = new Uri(Url.Link("DefaultApi", new { id = booking.RID }));
return response;
}
}
The JSON currently passed is:
var comment = {ID:0, HID: $('#HID').val(), RID:$('#RID').val(), Occ:$('#Occ').val(), CI:$('#CI').val(), CO:$('#CO').val(), CID:$('#CID').val()},{ID:0, HID: $('#HID').val(), RID:$('#RID').val(), Occ:$('#Occ').val(), CI:$('#CI').val(), CO:$('#CO').val(), CID:$('#CID').val()};
How can I pass multiple records to the controller, so that I don't have to call the JSON Post method may times?
You could modify the signature of your action to take an array of Booking:
public HttpResponseMessage PostBooking(Booking[] bookings)
{
...
}
and then send an array of booking from the client:
var bookings =
[
{
ID: 0,
HID: 'hid1',
RID: 'rid1',
Occ: 'occ1',
CI: 'ci1',
CO: 'co1',
CID: 'cid1'
},
{
ID: 1,
HID: 'hid2',
RID: 'rid2',
Occ: 'occ2',
CI: 'ci2',
CO: 'co2',
CID: 'cid2'
}
];

ASP.NET - JSON casting

var a = JSON.Deserialize<object>(e.ExtraParams["address"]);
The JSON
[{"id":"","country":"MYcOUNTRY","city":"citycitycity","street":"street","block":"house","building":"building","additionalInfo":"fdsfdsfdsfds","latitude":"32.9206000","longitude":"35.1003000"}]
and the class for storing information about address
[Serializable]
class BEAddress{
public string Id { get; set; }
public string Country { get; set; }
public string City { get; set; }
public string Street { get; set; }
public string Block { get; set; }
public string Building { get; set; }
public string Latitude { get; set; }
public string Longitude { get; set; }
public string AdditionalInfo { get; set; }
}
I can't savvy how to cast the object a to BEAddress?
UPDATE:
The way I seriliaze this data
<script type="text/javascript">
var <%=ClientID%>_getAddress = function () {
var jsonObj = [];
jsonObj.push({
Id: <%=AddressIdHdn.ClientID%>.getValue(),
Country: <%=cmbCountries.ClientID%> .getRawValue(),
City: <%=cmbCities.ClientID%> .getRawValue(),
Street: <%=txtStreet.ClientID%> .getValue(),
Block: <%=txtBlock.ClientID%> .getValue(),
Building: <%=txtBuilding.ClientID%> .getValue(),
AdditionalInfo: <%=txtAdditionalInfo.ClientID%> .getValue(),
Latitude: <%=txtLatitude.ClientID%> .getValue(),
Longitude: <%=txtLongitude.ClientID%> .getValue()
});
return jsonObj;
}
</script>
I would imagine you'd need to do the following:
BEAddress aVar = JSON.Deserialize<BEAddress>(e.ExtraParams["address"]);
At the moment you're turning your JSON into a bog standard 'object' so you'd need to turn the JSON into a specific 'BEAddress' object as per the example above.
What happens if you try the following:
var a = JSON.Deserialize<BEAddress>(e.ExtraParams["address"]);
What you're currently doing is deserializing into an generic object whereas you need a BEAddress.
Also place the [Serializable] attribute onto BEAddress

Resources