Entity Framework Select Query - c#

I have the following classes:
public class Seller : Entity
{
public int SellerId { get; set; }
public string Name { get; set; }
public ICollection<InventoryItem> InventoryItems { get; set; }
}
public class InventoryItem : Entity
{
public int InventoryId { get; set; }
public int SellerId { get; set; }
public string SellerSku { get; set; }
public ICollection<SiteInventoryItem> SiteInventoryItems { get; set; }
}
public class SiteInventoryItem : Entity
{
public int Id { get; set; }
public int InventoryId { get; set; }
public SiteType Site { get; set; }
}
So a Seller has a collection of InventoryItem which in turn have a collection of SiteInventoryItem.
How do I get a Seller with a list of InventoryItems that have a list of SiteInventoryItems where the SiteType == SiteType.SiteName and the Seller.SellerId == 14?
The SiteType is an Enum and the db type is int.
I have tried the following and it compiles and runs, but the result is not what is expected:
var seller = repo.Query(
x => x.InventoryItems,
x => x.InventoryItems.Select(y => y.SiteInventoryItems)
).Select(x => new
{
Seller = x,
InventoryItems = x.InventoryItems,
SiteInventoryItems = x.InventoryItems.Select(y => new
{
InventoryItem = y,
SiteInventoryItems = y.SiteInventoryItems.Where(z => z.Site == SiteType.Amazon)
})
}).Where(x => x.Seller.SellerId == 14).First();

var seller = repo.Query()
.Select(x => new
{
Seller = x,
InventoryItems = x.InventoryItems.Select(y => new
{
InventoryItem = y,
SiteInventoryItems = y.SiteInventoryItems.Where(z => z.Site == SiteType.Amazon)
}).Where(y => y.SiteInventoryItems.Any())
}).Where(x => x.Seller.Id == 14).First();
I've restructured the result type a bit, because I think it makes more sense this way. Now you see that the result only returns Sellers that have InventoryItems that have SiteInventoryItems.
Note that I also removed the arguments for repo.Query(). I assume that they serve as Includes. But I also hope that repo.Query() returns an IQueryable, so that the Where clause will be translated into the generated SQL. If so, the Includes are useless, because you're already querying the included entities because they are part of the anonymous type.

Related

Using LINQ SelectMany() to join multiple child tables without a navigation property

I kind of got confused on how to achieve what I want, and after searching in the internet, I think SelectMany() is the way to go, but I am getting lost on how to make this work (I have very weak understanding of how lambda expressions work I think)..
My goal is to be able to use LINQ to populate this class:
public class AttendanceList
{
public int AttendancePeriodId { get; set; } // from AttendancePeriod class
public int Activity { get; set; } // from DailyAttendance class
public string Name { get; set; } // from Employee class
public string Position { get; set; } // from Employee class
public string Department { get; set; } // from Employee class
}
I have a totally wrong and non-working code, but to illustrate, I wanna use something like:
var query = context.AttendancePeriod
.Include(i => i.DailyAttendance)
.Include(i => i.DailyAttendance).ThenInclude(ii => ii.Employee)
.Select(s => new AttendanceList
{
AttendancePeriodId = s.Id,
Activity = ...,
Name = ...,
Position = ...,
Department = ...
});
How do I use SelectMany() to achieve the above?
For reference, these are my classes:
public class AttendancePeriod
{
public int Id { get; set; }
public DateTime From { get; set; }
public DateTime To { get; set; }
public ICollection<DailyAttendance> DailyAttendances { get; set; }
}
public class DailyAttendance
{
public int Id { get; set; }
public Employee Employee { get; set; }
public TimeSpan TimeIn { get; set; }
public TimeSpan TimeOut { get; set; }
public string Activity { get; set;}
}
public class Employee
{
public int Id { get; set; }
public string Name { get; set; }
public string Position { get; set; }
public string Department { get; set; }
}
Untested and without any null checking:
var query = context.AttendancePeriod
// .Include(i => i.DailyAttendance)
// .Include(i => i.DailyAttendance).ThenInclude(ii => ii.Employee)
.SelectMany(s => s.DailyAttendances.Select(a =>
new AttendanceList
{
AttendancePeriodId = s.Id,
Activity = a.Activity ,
Name = a.Employee.Name,
Position = a.Employee.Position,
Department = a.Employee.Department,
}));
Maybe you are looking for this
First step get a flat list of all DailyAttendances
.SelectMany(x => x.DailyAttendances)
Now transform those into AttendanceList
.Select(x => new AttendanceList
{
//x is of type `DailyAttendance`
AttendancePeriodId = x.AttendancePeriod.Id,
Activity = x.Activity,
Name = x.Employee.Name,
Position = x.Employee.Position,
Department = x.Employee.Department,
}
If DailyAttendance doesn't have a member for AttendancePeriod you could do the following, instead of
.SelectMany(x => x.DailyAttendances)
use this, this will create a tuple contains x = AttendancePeriod and y = DailyAttendance
.SelectMany(x => x.DailyAttendances.Select(y => (x, y))
and now transform it to this
.Select(x => new AttendanceList
{
//x is of type `ValueTuple<AttendancePeriod, DailyAttendance>`
//x.Item1 is of type AttendancePeriod
//x.Item2 is of type DailyAttendance
AttendancePeriodId = x.Item1.Id,
Activity = x.Item2.Activity,
Name = x.Item2.Employee.Name,
Position = x.Item2.Employee.Position,
Department = x.Item2.Employee.Department,
}

Nesting EF Core Projection in EF Core Projection

Can I have a projection method/property, which I can use in another projection method/property?
I have Domain Entities Customer and Sale.
public class Customer : IEntity<long>
{
public long Id { get; set; }
public string Name { get; set; }
public virtual ICollection<Sale> Sales { get; set; }
}
public class Sale : IEntity<long>
{
public long Id { get; set; }
public decimal UnitPrice { get; set; }
public int Quantity { get; set; }
public decimal TotalPrice { get; set; }
public DateTimeOffset Date { get; set; }
public long CustomerId { get; set; }
public virtual Customer Customer { get; set; }
public long EmployeeId { get; set; }
public virtual Employee Employee { get; set; }
public long ProductId { get; set; }
public virtual Product Product { get; set; }
I know I can nest a projection as follows:
public class GetCustomerQuery : IGetCustomerQuery
{
private readonly IAppDbContext _context;
public GetCustomerQuery(IAppDbContext context)
{
_context = context;
}
public Task<CustomerModel> ExecuteAsync(long id)
{
return _context
.Customers
.Where(x => x.Id == id)
.Select(GetEmployeeProjection())
.SingleOrDefaultAsync();
}
private Expression<Func<Domain.Customers.Customer, CustomerModel>> GetEmployeeProjection()
{
return x => new CustomerModel
{
Id = x.Id,
Name = x.Name,
Sales = x.Sales.Select(y => new Sale
{
Employee = y.Employee.Name,
Product = y.Product.Name,
ProductCount = y.Quantity,
TotalPrice = y.TotalPrice
})
};
}
}
But can I separate the projections in two different method, f.e.:
private Expression<Func<Domain.Customers.Customer, CustomerModel>> GetEmployeeProjection()
{
return x => new CustomerModel
{
Id = x.Id,
Name = x.Name,
Sales = x.Sales.Select(GetSaleProjection())
};
}
private Expression<Func<Domain.Sales.Sale, Sale>> GetSaleProjection()
{
return y => new Sale
{
Employee = y.Employee.Name,
Product = y.Product.Name,
ProductCount = y.Quantity,
TotalPrice = y.TotalPrice
};
}
And is it more common to write the projection as a method or as a property?
EDIT: 2020-12-14
#joakimriedel, adding .AsQueryable() does kind of work, but I also need to add .ToList(), else a runtime exception was thrown (EF Core 5.0.1), as #Ivan Stoev did mention.
public Task<CustomerModel> ExecuteAsync(long id)
{
return _context
.Customers
.Where(x => x.Id == id)
.Select(_projection)
.SingleOrDefaultAsync();
}
private static readonly Expression<Func<Domain.Customers.Customer, CustomerModel>> _projection =
x => new CustomerModel
{
Id = x.Id,
Name = x.Name,
Sales = x.Sales
.AsQueryable()
.Select(_salesProjection)
.ToList()
};
private static readonly Expression<Func<Domain.Sales.Sale, Sale>> _salesProjection =
x => new Sale
{
Employee = x.Employee.Name,
Product = x.Product.Name,
ProductCount = x.Quantity,
TotalPrice = x.TotalPrice
};
Yes, but you would need to modify your expression slightly.
EF Core 3.1
private Expression<Func<Domain.Customers.Customer, CustomerModel>> GetEmployeeProjection()
{
return x => new CustomerModel
{
Id = x.Id,
Name = x.Name,
Sales = x.Sales.AsQueryable().Select(GetSaleProjection())
};
}
EF Core 5.0
private Expression<Func<Domain.Customers.Customer, CustomerModel>> GetEmployeeProjection()
{
return x => new CustomerModel
{
Id = x.Id,
Name = x.Name,
Sales = x.Sales.AsQueryable().Select(GetSaleProjection()).ToList()
};
}
The extra AsQueryable() is necessary since the IEnumerable implementation of Select only accepts Func<Domain.Sales.Sale, Sale> but IQueryable supports Expression<Func<Domain.Sales.Sale, Sale>>.
Forcing AsQueryable is useful also if you want to use expression predicates in subqueries such as Any().

How to use ICollection property to do a bool check

I have the following entities:
public class Order
{
public int Id { get; set; }
public ICollection<LineItem> LineItems { get; set; }
}
public class LineItem
{
public int Id { get; set; }
public int Qty { get; set; }
}
I would like to just check if an order has large items (qty > 50), so I would like to add a property to Order class like so:
public class Order
{
public int Id { get; set; }
public ICollection<LineItem> LineItems { get; set; }
public bool HasLargeItems
{
get
{
return LineItems.Any(l => l.Qty > 50);
}
}
}
This however, doesn't work because of lazy-loading. But if I use .Include(o => o.LineItems), it will fetch all the LineItems and then do an in-memory comparison. I would rather have this generate and execute a query against the db and just get me back a boolean value.
Is this possible to do as a property of Order entity, or do I have to write some other external query object which uses DbContext to perform these kinds of checks?
One way to do this is using anonymous select
var order = dbContext.Orders
.Where(o => o.Id == id)
.Select(o => new { Order = o, HasLargeItems = o.LineItems.Count > 50 })
.SingleOrDefault();
Try this it might help:
public bool HasItems
{
get{return LineItems.AsQueryable().Any(x => false);}
}

Cloning an object and saving the clone to db causes the original object to lose relational data

When I'm trying to clone a Product-object and save the clone to the database, the original object loses all it's relational data, such as ProductPropertyOptionForProducts, IdentifierForProducts and InCategories.
This is the Product model:
public class Product
{
public int Id { get; set; }
public int ProductGroupId { get; set; }
public int ProductGroupSortOrder { get; set; }
[Required, MaxLength(30), MinLength(4)] public string Title { get; set; }
[MaxLength(200)] public string Info { get; set; }
[MaxLength(4000)] public string LongInfo { get; set; }
[Required, DataType(DataType.Currency)] public decimal Price { get; set; }
public int Weight { get; set; }
public int ProductTypeId { get; set; }
public ICollection<ProductImage> Images { get; set; }
// Selected property options for this product
public ICollection<PropertyOptionForProduct> ProductPropertyOptionForProducts { get; set; }
// A product can have multiple identifiers (EAN, ISBN, product number, etc.)
public ICollection<IdentifierForProduct> IdentifierForProducts { get; set; }
public ProductType Type { get; set; }
public ICollection<FrontPageProduct> InFrontPages { get; set; }
public ICollection<ProductInCategory> InCategories { get; set; }
}
Some of the related models:
public class ProductInCategory
// A linking table for which products belongs to which categories
{
public int Id { get; set; }
public int ProductId { get; set; }
public int ProductCategoryId { get; set; }
public int SortOrder { get; set; }
// Nav.props.:
public Product Product { get; set; }
public ProductCategory ProductCategory { get; set; }
}
public class PropertyOptionForProduct
{
public int Id { get; set; }
public int ProductId { get; set; }
public int ProductPropertyId { get; set; }
public int ProductPropertyOptionId { get; set; }
// Nav.props.
public Product Product { get; set; }
public ProductPropertyOption ProductPropertyOption { get; set; }
}
public class IdentifierForProduct
{
public int Id { get; set; }
public int ProductId { get; set; }
public int ProductIdentifierId { get; set; }
[StringLength(30), MaxLength(30)]
public string Value { get; set; }
public ProductIdentifier ProductIdentifier { get; set; }
public Product Product { get; set; }
}
The original is loaded like this:
public async Task<Product> GetProduct(int Id)
{
Product DbM = await _context.Products
.Include(ic => ic.InCategories)
.ThenInclude(pc => pc.ProductCategory)
.Include(t => t.Type)
.ThenInclude(iit => iit.Identifiers) //ProductIdentifiersInTypes
.ThenInclude(i => i.Identifier) // ProductIdentifiers
.ThenInclude(ifp => ifp.ProductIdentifiers) // IdentifiersForProducts
.Include(t => t.Type)
.ThenInclude(pit => pit.Properties) // ProductPropertiesInTypes
.ThenInclude(p => p.Property) // ProductProperties
.ThenInclude(po => po.Options) // ProductPropertyOptions
.Include(p => p.ProductPropertyOptionForProducts)
.Where(p => p.Id == Id)
.SingleOrDefaultAsync();
return DbM;
}
This is the clone-method:
private async Task<Product> MakeClone(Product Original)
{
Product Clone = new Product
{
ProductGroupId = Original.ProductGroupId,
ProductGroupSortOrder = Original.ProductGroupSortOrder + 1,
IdentifierForProducts = Original.IdentifierForProducts,
Images = Original.Images,
InCategories = Original.InCategories,
Info = Original.Info,
InFrontPages = Original.InFrontPages,
LongInfo = Original.LongInfo,
Price = Original.Price,
ProductPropertyOptionForProducts = Original.ProductPropertyOptionForProducts,
ProductTypeId = Original.ProductTypeId,
Title = Original.Title,
Type = Original.Type,
Weight = Original.Weight
};
_context.Add(Clone);
await _context.SaveChangesAsync();
return Clone; // and go to the Edit-view.
}
Now, the clone has all the properties of the original product, but the original has been stripped of all of it's relational data. In the database, it looks like the clone's relational data has replaced the original's ones.
UPDATE
In accordance to Georg's answer, I changed my MakeClone()-method to this:
private Product MakeClone(Product Original)
{
List<IdentifierForProduct> identifiers = Original
.IdentifierForProducts
.Select(i => CloneIdentifierForProduct(i))
.ToList();
List<PropertyOptionForProduct> propertyOptions = Original
.ProductPropertyOptionForProducts
.Select(o => ClonePropertyOptionForProduct(o))
.ToList();
List<ProductInCategory> inCategories = Original
.InCategories
.Select(c => CloneProductInCategory(c))
.ToList();
List<FrontPageProduct> inFrontPages = Original
.InFrontPages
.Select(f => CloneFrontPageProduct(f))
.ToList();
List<ProductImage> images = Original.Images.Select(i => CloneProductImage(i)).ToList();
Product Clone = new Product
{
ProductGroupId = Original.ProductGroupId,
ProductGroupSortOrder = Original.ProductGroupSortOrder + 1,
Info = Original.Info,
LongInfo = Original.LongInfo,
Price = Original.Price,
ProductTypeId = Original.ProductTypeId,
Title = Original.Title,
Type = Original.Type,
Weight = Original.Weight,
IdentifierForProducts = identifiers,
ProductPropertyOptionForProducts = propertyOptions,
InCategories = inCategories,
InFrontPages = inFrontPages,
Images = images
};
_context.Add(Clone);
// fix FKs
foreach (var ifp in Clone.IdentifierForProducts) ifp.ProductId = Clone.Id;
foreach (var ofp in Clone.ProductPropertyOptionForProducts) ofp.ProductId = Clone.Id;
foreach (var pic in Clone.InCategories) pic.ProductId = Clone.Id;
foreach (var fpp in Clone.InFrontPages) fpp.ProductId = Clone.Id;
foreach (var pi in Clone.Images) pi.ProductId = Clone.Id;
// Lagre klonen i databasen:
_context.SaveChangesAsync();
return Clone;
}
... and added separate methods for cloning each of the linked data (I don't think I need to show all five methods):
private IdentifierForProduct CloneIdentifierForProduct(IdentifierForProduct ifp)
{
IdentifierForProduct IFP = new IdentifierForProduct
{
Product = ifp.Product,
ProductId = ifp.ProductId,
ProductIdentifier = ifp.ProductIdentifier,
ProductIdentifierId = ifp.ProductIdentifierId,
Value = ifp.Value
};
return IFP;
}
Now I'm getting ArgumentNullException on the creation of the child Lists.
Could it have something to do with the fact that for example IdentifierForProduct also has a child property (which I also want to clone)?
As Steve noted, you are just setting a reference to the collection properties of the original, instead of cloning the collection. If the relationship of the collection is not defined as many:many, this removes the related entities from the original and adds them to the clone.
For example, to clone the IdentifierForProducts collection, you have to clone each element and then add them to the collection of the clone.
Product clone = new Product {
IdentifierForProducts = Original.IdentifierForProducts.Select(ifp => MakeClone(ifp)).ToList(),
// other properties ....
};
// fix FKs after cloning
foreach (var ifp in clone.IdentifierForProducts) {
ifp.ProductId = clone.Id;
}
with MakeClone<IdentifierForProduct>(IdentifierForProduct original) analogous to MakeClone<Product>(Product original).

how to assign collection of classes to a new viewmodel

I have this scenario:
I want to make a ViewModel with the property that I only want but my issue is with the Collections. So here's an example:
Main classes:
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<Order> Orders { get; set; }
// remove some code for brevity
}
public class Order
{
public int Id { get; set; }
public string ItemName { get; set; }
public int CustomerId { get; set; }
public virtual Customer Customer { get; set; }
// remove some code for brevity.
}
View Models:
public class OrderVM
{
public string ItemName { get; set; }
}
public class CustomerVM
{
public string Name { get; set; }
public ICollection<OrderVM> Orders { get; set; }
}
Then I tried something like this:
_customerService.Include(c => c.Orders)
.Select(x => new CustomerVM
{
Name = x.Name,
Orders = x.Orders.Select(order => new OrderVM { ItemName = order.ItemName }) // but it says cannot convert implicitly from IEnumerable to ICollection
}
)
In a nutshell, how can I populate CustomerVM's properties? I only want to select that I want. Any thoughts? Really stuck here.
Linq .Select() generates IEnumerable<T> but your property is ICollection<T>. You can append .ToList() to the query to generate List<T> which is ICollection<T>.
customerService.Include(c => c.Orders)
.Select(x => new CustomerVM
{
Name = x.Name,
Orders = x.Orders
.Select(order => new OrderVM { ItemName = order.ItemName }).ToList()
}
Alternatively, if you do not specifically need Orders to be ICollection, then you could change your property definition to
public IEnumerable<OrderVM> Orders { get; set; }
just use ToList() to convert IEnumerable to ICollection. Not tested but it should work
customerService.Include(c => c.Orders)
.Select(x => new CustomerVM
{
Name = x.Name,
Orders = x.Orders.Select(order => new OrderVM { ItemName = order.ItemName }).ToList()
}

Categories