Entity Framework retrieve data from many to many relationship - c#

I have two tables - Order and MenuItem with many-to-many relation.
public class Order
{
[Key]
public int OrderId { get; set; }
public DateTime OrderDate { get; set; }
public string Comments { get; set; }
public string OrderStatus { get; set; }
public string WaiterName { get; set; }
public double TotalPrice { get; set; }
public virtual ICollection<MenuItem> MenuItems { get; set; }
}
public class MenuItem
{
[Key]
public int MenuItemId { get; set; }
public string Name { get; set; }
public double Price { get; set; }
public int MenuItemTypeId { get; set; }
public MenuItemType MenuItemType { get; set; }
public virtual ICollection<Order> Orders { get; set; }
}
public class OrderMenuItem
{
public int MenuItemId { get; set; }
public int OrderId { get; set; }
public Order Order { get; set; }
public MenuItem MenuItem { get; set; }
}
modelBuilder.Entity<MenuItem>()
.HasMany(m => m.Orders)
.WithMany(o => o.MenuItems)
.UsingEntity<OrderMenuItem>(
x => x.HasOne(x => x.Order)
.WithMany().HasForeignKey(x => x.OrderId),
x => x.HasOne(x => x.MenuItem)
.WithMany().HasForeignKey(x => x.MenuItemId)
);
The question is how can I retrieve MenuItems that are in certain order and show the data using the ListView?

Just use the navigation property.
The simple and easy way would be
var order = await _context.Orders
.Where(o => ...)
.Include(o => o.MenuItems)
.FirstOrDefaultAsync();
and the proper way would be, one that also lets you filter and order the items would be
var order = await _context.Orders
.Where(o => ...)
.Select(o => new OrderDto {
Id = o.Id,
Status = o.Status,
// etc
Items = o.Items
.Where(i => ...)
.OrderBy(i => ...)
// etc
})
.FirstOrDefaultAsync();
And while we're at it, delete that virtual modifier. It's used for lazy loading, and what you want here is not lazy loading.

Just to tack on to what Angius posted:
var orderedMenuItems = dbContext.MenuItems.OrderBy(mi => mi.id).ToList();
Then just connect your list to your UI.

Related

How to include associated entities with Entity Framework?

The database schema created has the following relations
The models used for generating the schema above are
public class Option
{
public int OptionId { get; set; }
public string OptionName { get; set; }
}
public class Value
{
public int ValueId { get; set; }
public string OptionValue { get; set; }
}
public class Sku
{
public int SkuId { get; set; }
public int ProductId { get; set; }
public decimal Price { get; set; }
[ForeignKey("ProductId")]
public Product Product { get; set; }
}
public class ProductVariant
{
public int Id { get; set; }
public int ProductId { get; set; }
public int OptionId { get; set; }
public int ValueId { get; set; }
public int SkuId { get; set; }
[ForeignKey("ProductId")]
public Product Product { get; set; }
[ForeignKey("OptionId")]
public Option Option { get; set; }
[ForeignKey("ValueId")]
public Value Value { get; set; }
[ForeignKey("SkuId")]
public Sku Sku { get; set; }
}
while the product class is
public class Product
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public IEnumerable<ProductVariant> ProductVariants { get; set; }
}
How can i load realated entities with this layout?
I tried the following but Options, Values and Skus are not accesible as navigation properties
var products = context.Products
.Include(x => x.ProductVariants)
.Include(x => x.Options)
.Include(x => x.Values)
.Include(x => x.Skus)
What changes should i make?
You lack navigation property in your product class:
public IEnumerable<Sku> Skus { get; set; }
And you need to use .ThenInclude instead of .Include when you are getting nested entities. It would be:
var products = context.Products
.Include(x => x.Skus)
.Include(x => x.ProductVariants)
.ThenInclude(ProductVariants => ProductVariants.Options)
.Include(x => x.ProductVariants)
.ThenInclude(ProductVariants => ProductVariants.Values)

Using Contains() on Included entity

I have the following code:
public PaginatedList<PdModel> PdModel { get; set; }
public async Task OnGetAsync(int id, int? pageIndex, string searchString)
{
IQueryable<PdModel> PdModelsQuer = _context.PdModel.Where(x => x.Id == id)
.Include(x => x.PdTables)
.Include(x => x.pdFolderTree)
.Include(x => x.PdReferences.Where(y=>y.ReferenceName.Contains(searchString)))
.Include(x => x.pdViews)
.Include(x => x.pdDomains)
.Include(x => x.PdModelSources)
.Include(x => x.pdModelExtendeds)
.Include(x => x.pdRules);
PdModel = await PaginatedList<PdModel>.CreateAsync(PdModelsQuer, 1, 10);
}
On code execution I am getting this error:
InvalidOperationException: The property expression 'x => {from PdReference y in x.PdReferences where [y].ReferenceName.Contains(__searchString_1) select [y]}' is not valid. The expression should represent a property access: 't => t.MyProperty'. For more information on including related data, see http://go.microsoft.com/fwlink/?LinkID=746393.
I guess I have to use Contains() on included property in another way. I tried a lot of things, but no reasonable code seems to be working.
Anybody can help me on this?
Thanks a lot in advance
My Domain models:
public class PdModel
{
[Key]
public int Id { get; set; }
public string ModelCode { get; set; }
public string ModelName { get; set; }
public string ModelComment { get; set; }
public string ModelDescription { get; set; }
public string ModelAnnotation { get; set; }
public string ModelDatabase { get; set; }
public DateTime? ModelCreationDate { get; set; }
public string ModelCreationUser { get; set; }
public DateTime? ModelModificationDate { get; set; }
public string ModelModificationUser { get; set; }
public string ModelGarantExtendedFlag { get; set; }
public string ModelColumnExtendedFlag { get; set; }
public string ModelTableExtendedFlag { get; set; }
public DateTime PdInsertedDate { get; set; }
public ICollection<PdRule> pdRules { get; set; }
public ICollection<PdModelExtended> pdModelExtendeds {get;set;}
public ICollection<PdTable> PdTables { get; set; }
public ICollection<PdReference> PdReferences { get; set; }
public ICollection<PdModelSource> PdModelSources { get; set; }
public ICollection<PdDomain> pdDomains { get; set; }
public ICollection<PdView> pdViews { get; set; }
[ForeignKey("Id")]
public virtual PdFolderTree pdFolderTree { get; set; }
}
public class PdReference
{
public int Id { get; set; }
public int ModelId { get; set; }
public string ModelCode { get; set; }
public string ReferenceCode { get; set; }
public string ReferenceName { get; set; }
public string ReferenceComment { get; set; }
public string ReferenceDescription { get; set; }
public string ReferenceAnnotation { get; set; }
public string ReferenceStereotype { get; set; }
public int ParentModelId { get; set; }
public string ParentModelCode { get; set; }
public string ParentTableCode { get; set; }
public int ParentTableId { get; set; }
public int ChildTableId { get; set; }
public string ChildTableCode { get; set; }
public string Cardinality { get; set; }
public DateTime PdInsertedDate { get; set; }
[ForeignKey("ModelId")]
public PdModel PdModels { get; set; }
public ICollection<PdJoin> pdJoins { get; set; }
[ForeignKey("ChildTableId")]
public virtual PdTable pdChildTable { get; set; }
You cannot filter an eagerly loaded relationship. The error you're getting is due to Include needing to be passed a valid property expression, which a Where clause is not.
If you only want to load a subset of this particular relationship, you'll need to explicitly load it. For example:
IQueryable<PdModel> PdModelsQuer = _context.PdModel.Where(x => x.Id == id)
.Include(x => x.PdTables)
.Include(x => x.pdFolderTree)
// remove this .Include(x => x.PdReferences.Where(y=>y.ReferenceName.Contains(searchString)))
.Include(x => x.pdViews)
.Include(x => x.pdDomains)
.Include(x => x.PdModelSources)
.Include(x => x.pdModelExtendeds)
.Include(x => x.pdRules);
foreach (var pdModel in PdModelsQuer)
{
var pdReferences = await _context.Entry(pdModel).Collection(x => x.PdReferences).Query()
.Where(x = x.ReferenceName.Contains(searchString)).ToListAsync();
}
If it's not obvious, this means issuing N+1 queries, where N is the count of your PdModels. In other words, the filtered collection has to be fetched for each instance individually.
However, based on querying by id, it appears that you should only have one matching PdModel. As such, you really shouldn't be using a Where here. Instead. Just add all your includes and then use SingleOrDefaultAsync:
var pdModel = await _context.PdModel
.Include(x => x.PdTables)
.Include(x => x.pdFolderTree)
.Include(x => x.pdViews)
.Include(x => x.pdDomains)
.Include(x => x.PdModelSources)
.Include(x => x.pdModelExtendeds)
.Include(x => x.pdRules)
.SingleOrDefaultAsync(x => x.Id == id);
Then, you can fetch the PdReferences for just this one instance:
var pdReferences = await _context.Entry(pdModel).Collection(x => x.PdReferences).Query()
.Where(x = x.ReferenceName.Contains(searchString)).ToListAsync();
It's important to note that this is being stored in another variable. Setting the filtered collection directly to your PdReferences property can cause side-effects, particularly if you end up trying to save this entity later, namely removing anything not in the filtered list from the database. In a situation like this, it's best to employ a view model and map over the data accordingly.

Adding data to crosstable created by EF with AutoMapper

It is my first a many-to-many relation consisting of Team, User and TeamUser objects. In TeamController I mapped TeamForCreationDto to Team, but ICollection Members was empty. Some bug in CreateMap?Q1: How it should be combined to fill all property and tables by EF? Now I have "for" loop and there created/added TeamUser.
Q2: If I must fill both property AdminId and Admin?
A2: No, after adding Admin, property AdminId in DB thanks EF will find value automatically.
public class Team
{
public int Id { get; set; }
public string Name { get; set; }
public int AdminId { get; set; }
public User Admin { get; set; }
//public int[] MembersId { get; set; }
public ICollection<TeamUser> Members { get; set; }
}
public class User
{
public int Id { get; set; }
public string Username { get; set; }
public ICollection<Team> TeamsAsAdmin { get; set; }
public ICollection<TeamUser> TeamsAsMember { get; set; }
}
public class TeamUser
{
public int TeamId { get; set; }
public Team Team { get; set; }
public int UserId { get; set; }
public User User { get; set; }
}
Relations between tables in ModelBuilder
builder.Entity<Team>()
.HasOne(t => t.Admin)
.WithMany(u => u.TeamsAsAdmin)
.OnDelete(DeleteBehavior.Restrict);
builder.Entity<TeamUser>()
.HasKey(tu => new { tu.TeamId, tu.UserId });
builder.Entity<TeamUser>()
.HasOne(tu => tu.User)
.WithMany(u => u.TeamsAsMember)
.HasForeignKey(tu => tu.UserId)
.OnDelete(DeleteBehavior.Cascade);
builder.Entity<TeamUser>()
.HasOne(tu => tu.Team)
.WithMany(t => t.Members)
.HasForeignKey(tu => tu.TeamId);
My CreateMap in AutoMapperProfiles()
CreateMap<TeamForCreationDto, Team>().ReverseMap().ForMember(u => u.MembersId, opt => opt.MapFrom(x => x.Members));
My TeamController.cs
public async Task<IActionResult> Create(int userId, TeamForCreationDto teamForCreationDto)
{
if (await _repoTeams.TeamExists(teamForCreationDto.Name))
return BadRequest("A team with this name already exists!");
var mappedTeam = _mapper.Map<Team>(teamForCreationDto);
//mappedTeam.AdminId = userId;
mappedTeam.Admin = await _repoUsers.GetUser(userId);
_repoTeams.Add(mappedTeam);
for (int i = 0; i < teamForCreationDto.MembersId.Length; i++)
{
TeamUser tm = new TeamUser();
tm.Team = mappedTeam;
tm.User = await _repoUsers.GetUser(teamForCreationDto.MembersId[i]);
_repoTeams.Add(tm);
}
await _repoTeams.SaveAll();
}
TeamForCreationDto.cs
public class TeamForCreationDto
{
int Id { get; set; }
public string Name { get; set; }
public string PhotoUrl { get; set; }
public int[] MembersId { get; set; }
}

Linq query to get all attributes of an item with a many to one relationship

I have two tables/Entities
public class Attribute
{
public int Id { get; set; }
public string Name { get; set; }
public string Details { get; set; }
public bool Inactive { get; set; }
public virtual ICollection<Item> Items{ get; set; }
}
public class Item
{
public int Id { get; set; }
public DateTime DateCreated { get; set; }
public DateTime? DateModified { get; set; }
public virtual ICollection<Attribute> Attributes { get; set; }
}
I am trying to figure out the Linq query to retrieve all of the attribute names for an item ID where the attributes Inactive status is set to false. The attribute count could be 0 to 20.
The following should give you all of the attribute names for the item with the specified ID.
var selectedId = 1; // The item ID you are looking for
var attrNames = items
.Where(i => i.Id == selectedId)
.SelectMany(x => x.Attributes)
.Where(a => !a.Inactive)
.Select(a => a.Name);

Why can I not do more than one level of include in my LINQ statement?

I am using Entity Framework 5 and I have these classes:
public partial class Subject
{
public int SubjectId { get; set; }
public string Name { get; set; }
public virtual ICollection<Topic> Topics { get; set; }
}
public partial class Topic
{
public int TopicId { get; set; }
public string Name { get; set; }
public int SubjectId { get; set; }
public virtual Subject Subject { get; set; }
public virtual ICollection<SubTopic> SubTopics { get; set; }
}
public partial class SubTopic
{
public int SubTopicId { get; set; }
public string Name { get; set; }
public int TopicId { get; set; }
public virtual Topic Topic { get; set; }
}
Now I am trying to write a LINQ query to populate this class:
public class TopicSubTopicSelect
{
public int TopicId { get; set; }
public int SubTopicId { get; set; }
public string TopicName { get; set; }
public string SubTopicName { get; set; }
}
So far I have this:
return _subjectsRepository
.GetAll()
.Where(s => s.SubjectId == subjectId)
.Include(s => s.Topics)
.Include(s => s.Topics.) <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
.AsEnumerable()
.Select(item => new TopicSubTopicSelect(item.TopicId,
item.SubTopicId,
item.Topic.Name,
item.Name))
.ToList();
But it gives me an error on the line where I put <<<<<
What I wanted was to have .Include(s => s.Topics.SubTopics)
However intellisense does not give me this as an option. Any ideas what I am
doing wrong and how I can modify the LINQ to get the data to fill the TopicSubTopicSelect class
This will give you desired result -
.Include(s => s.Topics.SelectMany(t => t.SubTopics))
Use .Select if SubTopic is a property but if is a list, use .SelectMany.
For more clarification refer to Select Vs SelectMany.
.Include(s => s.Topics.Select(t => t.SubTopics))
Use .Select() within the .Include() to get the desired join.
Making a simple project to test, I received the following:

Categories