0

I want to return a list of components, associated with a single equipment. I have 3 tables, Component, Equipment, EquipmentComponent.

One Equipment can have multiple Components, as seen in the diagram here. enter image description here

I have a method on a data service, which should return a list of components.

public async Task<IEnumerable<Component>> GetComponentsByEquipmentIdAsync(int equipmentId)
        {
            using (var ctx = _contextCreator())
            {
                //query components code
            }
        }

I made an SQL query which accomplishes this:

select Component.ComponentId, Component.ComponentName
from Equipment inner join EquipmentComponent on Equipment.EquipmentId = EquipmentComponent.EquipmentID
inner join Component on EquipmentComponent.ComponentID = Component.ComponentId
where Equipment.EquipmentId = equipmentId

But I am unsure how to turn this SQL query into LINQ to then return a list of components. Is it possible to achieve using only extensions methods? Currently I am returning a single component from the database without using LINQ:

return await ctx.Components.AsNoTracking().SingleAsync(f => f.ComponentId == componentId);
Leth
  • 1,033
  • 3
  • 15
  • 40
  • If the EF is setup correctly you should have an `EquipmentComponents` property on your `Component` and a `Equipments` on your `EquipmentComponent` class. You should be able to do something like `equipment.SelectMany(c => c.EquipmentComponents.Select(e => e.Components))` to get a flat list of components of the equipment – KinSlayerUY Dec 12 '17 at 14:58

2 Answers2

1

depends a little on your entities and how you've set up the M:N relationship ...

if you configured EF to be aware of the M:N relationship, then you can/should have a navigation property on either side that you can just use to get from one element from one side of the relationship to the related elements on the other side, and vice versa ...

in a code first approach it could look like this:

modelBuilder.Entity<EntityA>().HasMany(e => e.correspondingBs).WithMany(e => e.correspondingAs).Map(e => e.ToTable("AtoBTable").MapLeftKey("aId").MapRightKey("BId"));

in this case you can use a select like the one provided by KinSlayerUY in the comment to your question (EF handles the M:N part and hides the mapping table handling somewhere under the hood)

another option would be to treat the mapping table as an entity itself (maybe you want this, if the relation itself has additional properties)

in this case you would end up with something like:

ctx.Set<EntityA>().Where(x=>x.id==entityAid).SelectMany(x=>x.correspondingAtoBs.Select(y=>y.correspondingB))

(please be aware of typos in the code, I don't have an IDE at hand right now)

DarkSquirrel42
  • 10,167
  • 3
  • 20
  • 31
1

There are two types of LINQ functions: the ones that use deferred excecution and the ones that don't.

The functions that use deferred execution (Where, Select, GroupBy, Join, ...) only change the Expression of the IQueryable. It is only after you call a non-deferred function like ToList(), Any(), FirstOrDefault(), that the query actually is performed.

Async versions are only useful for non-deferred functions. After all, there is something in it that your thread could do something else instead of waiting for the results from the database.

Luckily there are awaitable async extension functions for DbSet. You can use them.

However, If you have setup your many-to-many relationship according to entity-framework default conventions your query would be very easy.

class Equipment
{
    public int Id {get; set;}
    ...
    // every equipment has zero or more Components (many-to-many)
    public virtual ICollection<Component> Components {get; set;}
}
class Component
{
    public int Id {get; set;}
    public string Name {get; set;}
    // Every Component belongs to zero or more Equipments (many-to-many)
    public virtual ICollection<Equipment> Equipments {get; set;}
}
class MyDbcontext : DbContext
{
    public DbSet<Equipment> Equipments {get; set;}
    public DbSet<Component> Components {get; set;}
}

That is all Entity Framework needs to know to understand that you want to design a many-to-many relationship. You don't have to mention the EquipmentComponent table. Entity framework will understand that it is needed and create it for you.

Back to your query
You want all ComponentIds and ComponentNames of all Components belonging to the Equipment that has Id equal to equipmentId

Having set-up your many-to-many correctly you can use the ICollections to fetch your data:

var result = dbContext.Equipments                     // from all equipment
    .Where(equipment => equipment.Id == equipmentId)  // take only those with equipmentId
    .SelectMany(equipment => equipment.Components     // from their components
        .Select(component => new
        {
            Id = component.Id,                        // select the Id
            Name = component.Name,                    // and the Name
        }));

The result of this is still an IQueryable. No communication is done with the database yet. To perform the query async you'll have to use a non-deferred function

 public async Task<IEnumerable<Component>> 
     GetComponentsByEquipmentIdAsync(int equipmentId)
{
    using (var dbContext = new MyDbContext())
    {
        var result = ... // see above
        return await result.ToListAsync();
    }
 }

If you really prefer to deviate from the standard entity framework many-to-many conventions, and you don't want to use the ICollections, but design your own joins, then you'll have to do them yourself.

Inner joins using linq method syntax are no problem, but inner joins look hideous if you join three tables. See How to perform multiple tables using lambda expressions

Therefore I'll use query syntax to create the IQueryable, then get the results using an async function:

public async Task<IEnumerable<Component>> GetComponentsByEquipmentIdAsync (int equipmentId)
{
    using (var dbContext = new MyDbContext())
    {
         var result = from equipment in dbContext.Equipments
         .Where(equipment => equipment.Id == equipmentId)
         join equipmentmodel in dbContext.EquipmentModel
            on equipment.Id equals equipmentModel.EquipmentId
         join model in dbContext.Models
            on equipmentmodel.ModelId equals model.Id
         select new
         {
             Id = component.Id,
             Name = component.Name
         };
    return await result.ToListAsync(); // or whatever non-deferred you want
}
Harald Coppoolse
  • 28,834
  • 7
  • 67
  • 116
  • Sorry for the late reply. Thanks, had to change a few things around in your IQueryable, but it works for me now. – Leth Dec 15 '17 at 11:24