Error using a subset of fields in Entity Framework - c#

I'm using EF 4 for the first time in a WebAPI service. I understand the basics if EF, but its still a bit of a learning curve when I'm used to using ADO.
I only want to return a subset (5) of over 100 fields in the table.
Model (EF Generated):
public partial class ITEM_MASTER
{
public string IM_ITEM_CODE { get; set; }
public string IM_UPC { get; set; }
public string IM_SUBDEPARTMENT { get; set; }
public string IM_DESC { get; set; }
...
}
Get Method in my Controller:
// GET api/Products
public IQueryable<ITEM_MASTER> GetProduct()
{
// return db.ITEM_MASTER;
return db.ITEM_MASTER.Select(x => new ProductList { ItemCode = x.IM_ITEM_CODE });
}
The return db.ITEM_MASTER works, but with the .Select method I get the error:
Cannot implicitly convert type
'System.Linq.IQueryable<JWebAPI.Models.ProductList>' to
'System.Linq.IQueryable<JWebAPI.Models.ITEM_MASTER>'. An explicit
conversion exists (are you missing a cast?)
public class ProductList
{
public string ItemCode {get; set;}
}
The ProductList class acts as a place holder object, as using the .Select returns a DBQuery not the original object type. I read that this was the ideal way to handle this situation. I'll add more properties to it when I've got it working. I have a downloaded sample that is structured the same way, and that works.
Is this the best way do do what I need, return only a subset of the EF fields? How can i resolve the conversion error?

Well the error is quite clear, your method is suppose to return IQueryable<ITEM_MASTER> but you are trying to return 'System.Linq.IQueryable<JWebAPI.Models.ProductList>'.
Since you projected your query's result to 'System.Linq.IQueryable<JWebAPI.Models.ProductList>' You need to have that as return type.
// GET api/Products
public IQueryable<ProductList> GetProduct() //Change return type
{
// return db.ITEM_MASTER;
return db.ITEM_MASTER.Select(x => new ProductList { ItemCode = x.IM_ITEM_CODE });
}
Since you are selecting only a subset of your original entity, you can't project to entity mapped to a table in Entity framework, that is why you need a place holder class ProductList

Related

Unable to map List<> navigational properties using OData, EF Core and AutoMapper

I'm currently writing an ASP .NET Core API utilizing OData for querying, and Entity Framework to talk to the database.
I want to separate the domain objects from the DTOs sent to the user, so have also started to use AutoMapper to translate entity framework query results to DTOs I have created.
At this point (while I'm testing), my DTOs and domain objects are the same - just public getter/setter properties. Examples of the DTOs are as follows:
public class NoteDTO
{
public int Id { get; set; }
public string Body { get; set; }
public string Conclusion { get; set; }
public string Title { get; set; }
public ManagerDTO Manager { get; set; }
}
public class ManagerDTO
{
public int Id { get; set; }
public virtual List<ProductDto> Products { get; set; }
}
public class ProductDto
{
public int Id { get; set; }
}
I also have a test method in my NotesController for fetching notes (again, using OData) which is as follows:
[HttpGet]
[EnableQuery]
public IQueryable<NoteDTO> GetMeeting()
{
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<Note, NoteDTO>();
cfg.CreateMap<Product, ProductDto>();
cfg.CreateMap<Manager, ManagerDTO>()
.ForMember(md => md.Products, conf => conf.MapFrom(m => m.Products));
});
return _context.Notes.ProjectTo<NoteDTO>(config);
}
I then try and hit my API with the following query:
https://localhost:5001/api/Notes?$select=Id,Body,Conclusion&$top=5&$expand=Manager($select=Id)
However, this fails, and in amongst the stack trace, I'm given the following error message:
System.ArgumentException: Expression of type 'System.Collections.Generic.IEnumerable`1[System.Tuple`3[TestEntityFramework.DataObjects.ProductDto,Microsoft.EntityFrameworkCore.Query.Internal.MaterializedAnonymousObject,Microsoft.EntityFrameworkCore.Query.Internal.MaterializedAnonymousObject]]' cannot be used for parameter of type 'System.Collections.Generic.IEnumerable`1[TestEntityFramework.DataObjects.ProductDto]' of method 'System.Collections.Generic.IEnumerable`1[TestEntityFramework.DataObjects.ProductDto] _ToEnumerable[ProductDto](System.Collections.Generic.IEnumerable`1[TestEntityFramework.DataObjects.ProductDto])'
If I remove the List from the ManagerDTO object and the relevant Product mapping config, the query above works successfully.
I saw this comment on a GitHub issue for what sounds like the same problem, but trying to implement the suggestion hasn't helped (assuming I've understood them correctly): https://github.com/AutoMapper/AutoMapper/issues/2853#issuecomment-482317381
Has anyone else run into this problem? I'm still getting used to AutoMapper so may have missed something obvious, but from searching around this seems to be a fairly uncommon issue and so pointers as to what's going on here have been hard to come by.
I'm open to any other suggestions as to what the best way of translating an OData query to entity framework, then back to a DTO is as well - if what I'm doing here isn't optimal!
Are you using the Automapper Collection Extensions? If not, this should solve your problem: https://github.com/AutoMapper/AutoMapper.Collection

ASP.NET Web API 2 CRUD Operation with Entity Framework

I am creating a MS Web API 2 project. I have created my Entity Framework in a separate project and am referencing it in my API. Reading over a few tutorials, it is suggested that:
"ideally, we should not return EF entity objects from the Web API. It is recommended to return DTO (Data Transfer Object) from Web API".
Hence, I have created my model in my API:
namespace MyAPI.Models
{
[Table("Customer")]
public class CustomerViewModel
{
[Key]
public int CustomerID { get; set; }
public string Code { get; set; }
public string Name { get; set; }
}
}
My question is: Do I need to create a data context class for each model in my API or is it fine to use EF context class? And if I do need to create a separate context for each model, how can I achieve this by a reference to the EF context class? Below is what I have started with:
namespace MyAPI.Models
{
public class CustomerDbContext : DbContext
{
public CustomerDbContext() : base("name=CusetomerDbContext")
{
}
public DbSet<MyEFDataAccess.Customer> CustomerViewModel { get; set; }
}
}
And my Controller is:
namespace MyAPI.Controllers
{
public class CustomersController : ApiController
{
private readonly CustomerDbContext _context = new CustomerDbContext();
// GET: api/Customer
public IQueryable<CustomerViewModel> GetCustomerViewModels()
{
return _context.CustomerViewModel;
}
}
The above correctly throws an error because it cannot convert EF customer to CustomerViewModel directly!
ideally, we should not return EF entity objects from the Web API. It
is recommended to return DTO (Data Transfer Object) from Web API.
The reason for this is to make sure you can change your DB schema without changing API, and vice versa. If you accomplish that goal, then you've adhered to that given advice.
The problem you're having is basic. Type A cannot be implicitly converted to Type B. As far as the compiler is concerned, you're trying to convert a DbContext to a FormControl, and it has no idea how to do that. You need to tell it explicitly what to do. One example, albeit not great:
public DbSet<MyEFDataAccess.Customer> Customer { get; set; }
and
public IQueryable<CustomerViewModel> GetCustomerViewModels()
{
return _context.Customer.Select(
customer => new CustomerViewModel
{
// <assign properties here>
}
);
}
That being said, returning an IQueryable<T> from your Controller is a definite no-no. You definitely want to allow for the consumer to query specific records. You could do this to enable pagination, for instance:
public async Task<List<CustomerViewModel>> GetCustomerViewModels(
int skip = 0,
int take = 100
)
{
return await _context.Customer
.Skip(skip)
.Take(take)
.Select(
customer => new CustomerViewModel
{
// <assign properties here>
}
)
.ToListAsync();
}

Mapping properties onto an IQueryable

We have an API with IQueryable<T> resources because we work with OData. My boss requires the resources to be exposed to also have additional information which is not available in the database.
OData allows one to only select the information they need, it does this by mapping a $select=Quantity query parameter to LINQ: performing a .Select(s => s.Quantity). This will of course not work since LINQ to Entities will complain the column does not exist in the database.
public class Item
{
public int Id { get; set; }
public int Quantity
{
get { return SubItems.Count; }
set {}
}
public virtual ICollection<SubItem> SubItems { get; set; }
}
I need a way around this restriction. I have looked into AutoMapper however it fails when using .ProjectTo() with their IValueResolver. I need IValueResolver because some of the extra properties are not so easily mapped onto a SQL structure, for example:
public class Item
{
public int Id { get; set; }
public string Description
{
get { return Convert(DescriptionId); }
set {}
}
public int DescriptionId { get; set; }
}
public string Convert(int value)
{
switch(value)
{
case 1:
return "1";
default:
return "0";
}
}
I have looked into the code below as well but this fails with System.ArgumentException: 'Cannot apply ODataQueryOptions of 'DtoItem' to IQueryable of 'Item'. Parameter name: query'. Reversing the data types I manage to get a result but I cannot return this of course as the data type is not matching with my controller function. I do however need the ODataQueryOptions of DtoItem otherwise I will not have access to $select any of those extra fields because it will not know about them. But then I am back to square one on how would OData even apply those options to an item of a different type.
public IQueryable<Item> Get(ODataQueryOptions queryOptions)
{
IQueryable<Item> items = (IQueryable<Item>) queryOptions.ApplyTo(repositoryItem.Select());
return mapper.Map<List<DtoItem>>(items.ToList()).AsQueryable(); // Cannot convert DtoItem to Item but of course I want to return DtoItem
}
What is necessary for me to accomplish this requirement? I have looked into Entity Framework but it seems very much sealed of. I'm not even sure if modifying the expression tree would be enough. I'd still have to add the values somehow during materialization.

Entity Framework ObjectQuery.Include()

I have an object with two objects as properties (User, PrimaryNode), both could potentially be null, see below:
public class Item
{
[Key]
public int ItemId { get; set; }
public string ItemName { get; set; }
public Node PrimaryNode { get; set; }
public User User { get; set; }
}
I'm using Entity Framework 6 to populate the Item object and using chained includes to populate the PrimaryNode and User objects within it.
When the first chained Include has a null object then the whole object returns as null, for example:
using (var db = new MyContext())
{
var item = db.Items.Include(i => i.User).Include(n => n.PrimaryNode).FirstOrDefault(i => i.ItemId == id);
}
If in the above example i.User is null then the item variable is null. Whats the best way of populating both the sub-objects in a way that if a sub-object is null then the parent object and the other sub-object will still be populated?
I don't think your issue is due to the Include calls. According with the documentation:
This extension method calls the Include(String) method of the
IQueryable source object, if such a method exists. If the source
IQueryable does not have a matching method, then this method does
nothing.
In other words is going to be translated to:
var item = db.Items.Include("User").Include("PrimaryNode").FirstOrDefault(i => i.ItemId == id);
My question is, are you sure you have an Item with that id properly related with existing rows in Users and PrimaryNodes tables in your DB?. When you call Include method at the end is going to be translated to a join, so if the FK of your relationship doesn't match with the PK that reference, your query should not return what you are expecting.
Anyways, if you want to try another variant to load related properties you can use Explicit Loading:
var item = db.Items.FirstOrDefault(i => i.ItemId == id);
context.Entry(item).Reference(p => p.PrimaryNode).Load();
context.Entry(item).Reference(p => p.User).Load();
I think it would be better if you use Lazy loading int his situation. Just make the User and PrimaryNode virtual:
public class Item
{
[Key]
public int ItemId { get; set; }
public string ItemName { get; set; }
public virtual Node PrimaryNode { get; set; }
public virtual User User { get; set; }
}
And then:
var db = new MyContext();
var item = db.Items.FirstOrDefault(i => i.ItemId == id);
As others have mentioned, I think your issue is not due to the Includes. However, I think the following method has value. It is functionally equivalent to what you are already doing with the chained includes, but I think it has several benefits including making the intention of the code clear to the user.
The includes can be placed in Extension methods:
using System.Data.Entity;
using System.Linq;
namespace Stackoverflow
{
public static class EntityExtensions
{
public static IQueryable<Item> IncludePrimaryNode(this IQueryable<Item> query)
{
// eager loading if this extension method is used
return query.Include(item => item.PrimaryNode);
}
public static IQueryable<Item> IncludeUser(this IQueryable<Item> query)
{
// eager loading if this extension method is used
return query.Include(item => item.User);
}
}
}
Then, you can use the extensions as follows:
using (var db = new MyContext())
{
var itemQuery = db.Items.IncludeUser();
itemQuery = itemQuery.IncludePrimaryNode();
var item = itemQuery.FirstOrDefault(i => i.Id == 1);
}
It's just another way of doing the same thing, but I like the clarity it adds to the code.

Dynamic Expression for Ordering Child Collections with Entity Framework

I am new to EF. I am trying to get Entity Framework 4.2 to do a sort by a calculated property (not mapped).
Here is what my entity look like:
public class Site : Entity
{
public Site()
{
Equipments = new HashSet<Equipment>();
Forecasts = new HashSet<Forecast>();
}
[StringLength(8)]
public string Number { get; set; }
[StringLength(50)]
public string EquipmentShortCLLI { get; set; }
[StringLength(50)]
public string Location { get; set; }
public virtual Central Central { get; set; }
public virtual ICollection<Equipment> Equipments { get; set; }
public virtual ICollection<Forecast> Forecasts { get; set; }
#region Calculated Items
public bool IsEmbargo {
get { return Equipments.Count > 0 && Equipments.SelectMany(x => x.EquipmentDetails).Any(e => e.IsEmbargo); }
}
//...
public int PortsCapacity
{
get
{
return Equipments.Count > 0
? Equipments.SelectMany(x => x.Slots).Sum(x => x.PortsCapacity)
: 0;
}
}
#endregion
//...
By trying to order using any of my readonly properties I am getting the exception:
The specified type member 'PortsCapacity' is not supported in LINQ to Entities. Only initializers, entity members, and entity navigation properties are supported.
Which makes sense because EF is trying to build an sql orderby with a field that does not exist in the database (my understanding..).
Now, by using some dynamic linq code I was able to make this work for my many-to-one columns by passing "Central.SomeField" (as opposed to making a ReadOnly Property that returns Central.SomeField).
I.E.:
query.OrderBy("Central.SomeField");
However, I still face the same issue when it comes to a collection of items (Equipments). I am trying to make this as dynamic as possible by using a string coming from the client side and avoiding a long switch case, but at this point I will accept any ideas, so long as the sorting happens on the database side.
Edit 1:
Following what Ladislav Mrnka says, how would one execute an OrderBy clause on one-to-many child items using lambdas or expression?
I don't think that Dynamic Linq is capable of this. You need a real Linq subquery to compute aggregations on Equipements so it will simply not work. If the user selects ordering by IsEmbargo or PortsCapacity you must have some switch / if block to handle this case by appending special part of the query - no other way.

Categories

Resources