linq query regarding many to many relation - c#

I have a many to many relation set up this way
public class Produto
{
public int ProdutoID { get; set; }
public string Nome { get; set; }
public double Preco { get; set; }
public double Altura {get ; set; }
public double Largura {get ; set; }
public double Profundidade {get ; set; }
public virtual ICollection<ProdutoParte> ProdutoPartes { get; set; }
}
public class Parte
{
public int ParteID { get; set; }
public string Nome { get; set; }
public double Preco { get; set; }
public double Altura {get ; set; }
public double Largura {get ; set; }
public double Profundidade {get ; set; }
public virtual ICollection<ProdutoParte> ProdutoPartes { get; set; }
}
public class ProdutoParte
{
public int ProdutoID { get; set; }
public Produto Produto { get; set; }
public int ParteID { get; set; }
public Parte Parte { get; set; }
}
This is my dbcontext
public DbSet<Produto> Produtos { get; set; }
public DbSet<Parte> Partes {get;set;}
public DbSet<ProdutoParte> ProdutoPartes { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<ProdutoParte>()
.HasKey(pp => new { pp.ProdutoID, pp.ParteID });
modelBuilder.Entity<ProdutoParte>()
.HasOne(pp => pp.Produto)
.WithMany(pr => pr.ProdutoPartes)
.HasForeignKey(bc => bc.ProdutoID);
modelBuilder.Entity<ProdutoParte>()
.HasOne(pp => pp.Parte)
.WithMany(pa => pa.ProdutoPartes)
.HasForeignKey(pp => pp.ParteID);
}
One Produto(product) can be composed of many Parte (parts) and
one Parte (part) can be used to create many Produto(products).
I'm trying to write the following query:
produto/{id}/partes
That returns all the parts being used in the product with {id}
This is what I have now
public IActionResult GetPartes([FromRoute] int id)
{
if(!ModelState.IsValid)
{
return BadRequest(ModelState);
}
Produto produto = _context.Produtos.SingleOrDefault(p => p.ProdutoID == id);
if(produto == null)
{
return NotFound();
}
var parts = _context.ProdutoPartes.Where(p => p.ProdutoID == id);
return Ok(parts);
}
With the following test data (still using inmemorydatabse)
if (_context.Produtos.Count() == 0)
{
_context.Produtos.Add(new Produto() {ProdutoID=1, Nome="product1" });
_context.Partes.Add(new Parte() {ParteID=1, Nome="part1" });
_context.Partes.Add(new Parte() {ParteID=2, Nome="part2" });
_context.Partes.Add(new Parte() {ParteID=3, Nome="part3" });
_context.ProdutoPartes.Add(new ProdutoParte() {ProdutoID=1, ParteID=1 });
_context.ProdutoPartes.Add(new ProdutoParte() {ProdutoID=1, ParteID=2 });
_context.ProdutoPartes.Add(new ProdutoParte() {ProdutoID=1, ParteID=3 });
_context.SaveChanges();
}
However using postman over json all I get is this when calling
get api/produto/1/partes
[{"produtoID":1,"produto":{"produtoID":1,"nome":"product1","preco":0.0,"altura":0.0,"largura":0.0,"profundidade":0.0,"produtoPartes":[
When it should be a list of parts.
Would appreciate any help.

When it should be a list of parts.
You aren't asking for a list of parts: you're asking for a list of ProdutoPartes. There are a few ways you can change your query to get what you want. One is:
var parts = _context.Partes
.Where(p => p.ProdutoPartes.Any(pp => pp.ProdutoID == id))
.ToList();
A few other suggestions:
Create a DTO class that represents just the data you really want to send back as part of your API. Use a .Select() statement to map from your Entities to this DTO. That way you don't get tons more information sent back than you really intended to.
It looks like ProdutoPartes is really just an intermediate mapping table. Remove that from your model, in favor of a many-to-many mapping between Produto and Parte. This way, your query becomes:
var parts = _context.Partes
.Where(p => p.Produtos.Any(pp => pp.ProdutoID == id))
.ToList();

This query will give you all the parts for the specified ProdutoID.
List<Parte> parts = _context.ProdutoPartes.Where(x => x.ProdutoID == id)
.Select(x = x.Parte).ToList();
Is the data in your Parte class sufficient enough? Does it have all the info the client needs? If yes, then you are good.
If not, create a ParteDto:
public class ParteDto
{
public Parte Parte { get; set; }
// Put other properties here
}
Modify your query:
List<ParteDto> parts = _context.ProdutoPartes.Where(x => x.ProdutoID == id)
.Select(x = new ParteDto { Parte = x.Parte /* , other properties */ })
.ToList();
To be honest, if you are speaking to the DbContext directly from your controller, you do not even need a DTO and you can just return an anonymous type like below. Please note there is no type after the new keyword:
var parts = _context.ProdutoPartes.Where(x => x.ProdutoID == id)
.Select(x = new { Parte = x.Parte /* , other properties */ })
.ToList();

Related

Linq with Lambda - how do I restrict joined table rows?

I want to use Linq to duplicate this T-SQL query on a sports teams database, to look up the experienced players in handball teams:
Select TE.TeamName, PL.FirstName, PL.LastName
From T_Team as TE
Inner Join T_Player As PL
On PL.Team_ID = TE.Team_ID
And PL.ExpLevel = 'Experienced'
Where TE.SportName = 'Handball'
I've tried creating two entities for my two tables:
public class TTeam
{
public int TeamId { get; set; }
public string TeamName { get; set; }
public string SportName { get; set; }
public virtual List<TPlayer> TeamPlayers { get; set; }
// Called in the context OnModelCreating() method
public static void CreateModel(EntityTypeBuilder<TTeam> p_ebpTable)
{
p_etbTable.ToTable("T_TEAM");
p_etbTable.HasKey(t => new { t.TeamId }).HasName("PK_TEAMID_T_TEAM");
// Column definitions
// Foreign Keys
p_etbTable.HasMany(t => t.TeamPlayers).
WithOne(p => p.CurrentTeam).
HasPrincipalKey(t => t.TeamId).
HasForeignKey(p => p.TeamId);
}
}
and
public class TPlayer
{
public int PlayerId { get; set; }
public int TeamId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string ExpLevel { get; set; }
public virtual TTeam CurrentTeam { get; set; }
// Called in the context OnModelCreating() method
public static void CreateModel(EntityTypeBuilder<TPlayer> p_ebpTable)
{
p_etbTable.ToTable("T_PLAYER");
p_etbTable.HasKey(t => new { t.PlayerId }).HasName("PK_PLAYERID_T_PLAYER");
// Column definitions
// Foreign Keys
p_etbTable.HasOne(p => p.CurrentTeam).
WithMany(t => t.TeamPlayers).
HasForeignKey(p => p.TeamId).
HasPrincipalKey(t => t.TeamId);
}
}
then use them in
using Microsoft.EntityFrameworkCore;
IEnumerable<TTeam> z_enbHandballTeams = z_dbcDbContext.TTeamRepository
.Where(te => te.SportName == "Handball")
.Include(te => te.TeamPlayers.Where(pl => pl.ExpLevel == "Experienced"));
but looping through z_enbHandballTeams in a foreach, throws an InvalidOperationException with the message "Lambda expression used inside Include is not valid".
(I guess it goes without saying that ExpLevel is a number and SportName is actually SportId, but I felt it would look easier to read that way.)
What am I doing wrong?
EF Core 3.1.x do not support filtered Include. Workaround is to do that via Select
var z_enbHandballTeams = z_dbcDbContext.TTeamRepository
.Where(te => te.SportName == "Handball")
.Select(te => new TTeam
{
TeamId = te.TeamId,
TeamName = te.TeamName,
SportName = te.SportName,
TeamPlayers = te.TeamPlayers.Where(pl => pl.ExpLevel == "Experienced")
.ToList()
});

Why result of IQueryable type converts to empty object?

public IHttpActionResult GetAllCollections(CollectionsDTO collectionsDTO)
{
if (!ModelState.IsValid)
return BadRequest();
try
{
var collectionsSectionRolesFlatDTO = (from c in db.Collections
join sr in db.SectionRole
on c.SectionRoleId equals sr.Id
select new CollectionsSectionRolesFlatDTO
{
Collections = new CollectionsDTO
{
CollectionTitleAr = c.CollectionTitleAr,
CollectionTitleEn = c.CollectionTitleEn,
CoverImagePath = c.CoverImagePath,
SectionRoleId = c.SectionRoleId,
},
SectionRole = new SectionRoleDto
{
NameAr = sr.NameAr,
NameEn = sr.NameEn
}
})
.AsQueryable();
var collectionsDto = Mapper.Map<List<CollectionsSectionRolesDTO>>(collectionsSectionRolesFlatDTO);
return Ok(collectionsDto);
}
catch (Exception ex)
{
return BadRequest("GetAllCollection: "+ ex.ToString());
}
}
DTOs:
public class CollectionsDTO
{
public int Id { get; set; }
public string CollectionTitleEn { get; set; }
public string CollectionTitleAr { get; set; }
public string CoverImagePath { get; set; }
public int? SectionRoleId { get; set; }
}
public class CollectionsSectionRolesFlatDTO
{
public SectionRoleDto SectionRole { get; set; }
public CollectionsDTO Collections { get; set; }
}
public class CollectionsSectionRolesDTO
{
public int Id { get; set; }
public string CollectionTitleEn { get; set; }
public string CollectionTitleAr { get; set; }
public string CoverImagePath { get; set; }
public int? SectionRoleId { get; set; }
//section role
public string NameAr { get; set; }
public string NameEn { get; set; }
}
public class SectionRoleDto
{
public int Id { get; set; }
public string NameEn { get; set; }
public string NameAr { get; set; }
}
Automapper:
CreateMap<Collections, CollectionsDTO>();
CreateMap<CollectionsSectionRolesFlatDTO, CollectionsSectionRolesDTO>();
Now the problem is mindboggling.
this line
var collectionsDto = Mapper.Map<List<CollectionsSectionRolesDTO>>(collectionsSectionRolesFlatDTO);
is supposed to return the data but instead it returns what I have passed in the function param.
Why is this happening? I have spent literally hours but nothing. I checked all the automapper setting but this doesn't work.
I am new to c# so any help regarding it would be appreciated. thanks.
Why would you be going through the trouble of double projection? Just configure Automapper with the information to get a desired DTO out of the object graph. Ensure you have navigation properties set up for your relationships to make querying against the object graph a lot simpler. Linq & EF does not need to be written as a substitute for SQL. (explicit joins) Navigation properties allow EF to provide those joins entirely behind the scenes for simpler querying.
First, the mapping:
CreateMap<Collections, CollectionsSectionRolesDTO>()
// Id, NameAr, and NameEn should auto-resolve.
.ForMember(x => x.CollectionTitleEn, opt => opt.MapFrom(src => src.Collection.CollectionTitleEn))
.ForMember(x => x.CollectionTitleAr, opt => opt.MapFrom(src => src.Collection.CollectionTitleAr));
// Continue for fields coming from Collection...
then to query using the automapper config. (config)
var results = db.Collections
.ProjectTo<CollectionsSectionRolesDTO>(config)
.ToList();
Done and dusted. No need to select and flatten data first into memory then use Automapper to create a new in-memory collection of the desired view model. Just project down to the desired view model directly within the query.
Double-projection is useful where you need to flatten data down first in order to perform conversions or transforms that cannot be converted in SQL. The first projection (Select) would typically be to an anonymous type or a DTO using ProjectTo, then fed into code that would provide the necessary transformations to produce the end result view models/DTOs.

Query a parent-children entities with one-many relationships and retrieve children records based on values in the parent record using LINQ to Entities

I have a Asp.net MVC application which has 3 classes; Event, Modification, FieldHistory. Events has a one-many with Modifications, and Modifications also has one-many relationship with FieldHistory.
Event class:
public class Event
{
public int EventId { get; set; }
public string EventName { get; set; }
public DateTime? EventDate { get; set; }
}
Modification class:
public class Modification
{
public int ModificationId { get; set; }
public int EventId { get; set; }
public virtual Event Event { get; set; }
public string ModificationStatus { get; set; }
}
FieldHistory class:
public class FieldHistory
{
public int FieldHistoryId { get; set; }
public int ModificationId { get; set; }
public virtual Modification Modification { get; set; }
public int PropValId { get; set; }
public int KeyFieldId { get; set; }
public string FieldName { get; set; }
public string Instructions { get; set; }
public string Format { get; set; }
}
In one of the Index actions I am returning an IEnumerable of Modifications to the view and present them as list items in the index page.
public async Task<ActionResult> Index()
{
var modifications = _applicationDbContext.Modifications.Include(m
=> m.Event)
.Include(m => m.ItemType)
.Include(m => m.ModificationType)
.OrderByDescending(m => m.CurrentUserId);
return View(await modifications.ToListAsync());
}
I would like to extend this query so that now I will also show Modifications together with one of its child records based on the ModificationStatus field of the Modification entity. For example if the modificationstatus is "Creating" or "Created" then load the FieldHistory that has PropValId of 1, otherwise if status is "Processing" or "Approved" then load FieldHistory of PropValId of 2 as its child. So in a list item I should have all 3 entities in a record: Event, Modification, FieldHistory
I am a just a beginner in Linq and hope that this can be another big step for me.
Not sure if there is a direct way to tie in the ModificationStatus with PropValId, so instead just using what you conveyed above and obviously you'd have to supply the additional rules if there are more... also only going to take the First one of other object types but you can change that if you need all associated ones...
I'm picking up from your variable modifications as defined above, and of course assuming the names of the tables in your DbContext containing the other objects:
var trios = modifications.Select(m => new { Modification = m, // select the modification obj itself
FieldHistory = _applicationDbContext.FieldHistories.FirstOrDefault(fh =>
fh.ModificationId == m.ModificationId && // I am assuming this is required
fh.PropValId == (new List<string> {"Creating", "Created"}.Contains(m.ModificationStatus) ? 1 : 2)),
Event = _applicationDbContext.Events.FirstOrDefault(e => e.EventId == m.EventId)
}).ToList();
This will return what you're looking for. If you want, you can create a class, e.g.:
public class Trio {
public Modification Modification { get; set; }
public FieldHistory FieldHistory { get; set; }
public Event Event { get; set; }
public Trio (Modification modification, FieldHistory fieldHistory, Event #event)
{
this.Modification = modification;
this.FieldHistory = fieldHistory;
this.Event = #event;
}
}
Then change to:
var trios = modifications.Select(m => new Trio(m, // select the modification obj itself
_applicationDbContext.FieldHistories.FirstOrDefault(fh =>
fh.ModificationId == m.ModificationId && // I am assuming this is required
fh.PropValId == (new List<string> {"Creating", "Created"}.Contains(m.ModificationStatus) ? 1 : 2)),
_applicationDbContext.Events.FirstOrDefault(e => e.EventId == m.EventId)
)).ToList();

Compare object with an array with another array

I have a model Group:
public class GroupModel
{
[Key]
public int GroupModelId { get; set; }
[Required]
[MaxLength(50)]
[DataType(DataType.Text)]
public string GroupName { get; set; }
[Required]
public virtual ICollection<FocusArea> FocusAreas { get; set; }
...
And a model Focus:
public class FocusArea
{
public int FocusAreaId { get; set; }
public FocusEnum Focus { get; set; }
public List<ApplicationUser> ApplicationUser { get; set; }
public virtual ICollection<GroupModel> GroupModel { get; set; }
public enum FocusEnum
{
Psych,
Medical,
LivingWith
}
Group and Focus has a many-to-many relationship. My Controller is receiving:
public ActionResult GroupSearch(string[] focusSelected) // Values possible are Pysch, Medical and LivingWith
{
List<GroupModel> groups;
...
Problem: I want to select the groups that have all the focus that are inside the focusSelected array.
What I've tried:
groups = groups.Where(t => t.FocusAreas.Where(x => focusSelected.Contains(x.Focus))).ToList()).ToList();
Obviously not working. Does anyone have another idea?
This may help you
var result = groups.Where(g => g.FocusAreas.All(f => focusSelected
.Any(fs => (FocusEnum)Enum.Parse(typeof(FocusEnum), fs, true) == f.Focus)));
Where needs a delegate / expression that returns bool. In your sample - you are putting Where inside Where, where Where returns collection.
Changing inner Where to All should do the trick:
var allSelParsed = focusSelected.Select(s => (FocusEnum)Enum.Parse(typeof(FocusEnum), s)
.ToList();
groups = groups.Where(gr => allSelParsed.All(selected =>
gr.FocusAreas.Any(fc =>
fc.Focus == selected)))
.ToList();
This should give you expected result
var result = groups.Where(g =>
focusSelected.All(fs =>
g.FocusAreas.Any(fa => fa.ToString() == fs)));

Mapping a single child

The mapping below works, but I was wondering if it can be done with less configuration. I've tried playing around with ForAllMembers and ForSourceMember but I haven't found anything that works so far.
Classes
public class User
{
[Key]
public int ID { get; set; }
public string LoginName { get; set; }
public int Group { get; set; }
...
}
public class UserForAuthorisation
{
public string LoginName { get; set; }
public int Group { get; set; }
}
public class Session
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid ID { get; set; }
public virtual User User { get; set; }
...
}
Configuration
Mapper.CreateMap<Session, UserForAuthorisation>()
.ForMember(u => u.LoginName, m => m.MapFrom(s => s.User.LoginName))
.ForMember(u => u.Group, m => m.MapFrom(s => s.User.Group));
Query
UserForAuthorisation user = this.DbContext.Sessions
.Where(item =>
item.ID == SessionID
)
.Project().To<UserForAuthorisation>()
.Single();
Edit This works for the reverse.
Mapper.CreateMap<UserForAuthorisation, User>();
Mapper.CreateMap<UserForAuthorisation, Session>()
.ForMember(s => s.User, m => m.MapFrom(u => u));
var source = new UserForAuthorisation()
{
Group = 5,
LoginName = "foo"
};
var destination = Mapper.Map<Session>(source);
Unfortunately, Reverse() isn't the easy solution, mapping doesn't work.
Mapper.CreateMap<UserForAuthorisation, User>().ReverseMap();
Mapper.CreateMap<UserForAuthorisation, Session>()
.ForMember(s => s.User, m => m.MapFrom(u => u)).ReverseMap();
var source = new Session()
{
User = new User()
{
Group = 5,
LoginName = "foo"
}
};
var destination = Mapper.Map<UserForAuthorisation>(source);
I can see only one option to do less configurations. You can use benefit of flattering by renaming properties of UserForAuthorisation class to:
public class UserForAuthorisation
{
public string UserLoginName { get; set; }
public int UserGroup { get; set; }
}
In this case properties of nested User object will be mapped without any additional configuration:
Mapper.CreateMap<Session, UserForAuthorisation>();

Categories

Resources