Dynamic Expression for Ordering Child Collections with Entity Framework - c#

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.

Related

Joining Entity Framework tables using Linq when IDs do not exist in both tables

Below is a class I have used to generate a table in my database using Entity Framework. I'd like to be able to link this table to another table, Property. However, the way my code is set up there is not an Id column in the Instruction table, there is a Property property within the class, which then generates a PropertyId column in the actual database, but since the Property property is not an Id I am unable to using Linq to join these tables.
Instruction table
[Table("Instruction")]
public class Instruction
{
[Key]
public int Id { get; set; }
public InstructionTypes InstructionType { get; set; }
public Property Property { get; set; } //Generates the EF property FK, but is not an ID so therefore cannot be used in linq.
}
Property table
[Table("Property")]
public partial class Property
{
[Key]
public int Id { get; set; }
public Address Correspondence { get; set; }
}
Join Query
var instruction =
from instructions in _context.Instructions
join properties in _context.Properties on instructions.Property equals properties.Id
where ...
The above query gives a compiler error of: `The type of one of the expressions in the join clause is incorrect.
This error is being generated as I'm attempting to use a property object to join with a propertyId.
How can I alter this query so that I am able to join these two tables?
In 99% of all cases, you do not want to use the join operator. Entity Framework automatically generates SQL JOINS for you when you are using Navigation Properties.
var instruction = await _context.Instructions.Where(i => i.Property...).FirstOrDefaultAsync().ConfigureAwait(false);
Note, that depending on whether you are using EF6 or EF Core or with different configuration, Lazy Loading may be disabled (if not, I strongly encourage you to disable it as it is a massive performance bottleneck).
So you have to use the Include Method to eagerly load the related entity.
var instruction = await _context.Instructions.Include(i => i.Property).Where(i => i.Property...).FirstOrDefaultAsync().ConfigureAwait(false);
But before doing this, think if you really need the Instruction. If not, your code could become:
var property = await _context.Properties.Where(p => p.Instructions.Any(i => ...)).FirstOrDefaultAsync().ConfigureAwait(false);
Please note that you have to extend your Property class for this to work to have a back-reference
public partial class Property
{
// No need for the Key attribute, as this is convention
public int Id { get; set; }
public Address Correspondence { get; set; }
public int CorrespondenceId { get; set; } // Not needed in this scenario, but good practice
public ICollection<Instruction> Instructions { get; } = new HashSet<Instruction>();
}
You seems to be a newcomer to linq. As such you are still thinking as if you still are in an sql world.
With linq to entities, the use of join is the exception. SQL join are generated silently by EF using the navigation properties.
So your query can be:
var instruction =
from instruction in _context.Instructions
where instruction.Porperty.Correspondence.Contains("abc");
then you can access
instruction.First().Property.Correspondence
As a good practice you can delclare the foreign keys as class members and use the fluent API to bind them.
To test you can use the following code,
//assuming that Instructions is a DbSet<Instruction>
using (var context = new MyContext() ) {
context.Instructions.Add(
new instruction {
Property = new Property {
Correspondence = new Address {}
}
});
}
using (var context = new MyContext() ) {
var c = context.Instructions.First();
console.WriteLine($"{c.Id}, {c?.Property.Id}, {c?.Property?.Correspondence.Id}");
});

EF FromSql() with related entities

Disclaimer: these requirements are not set by me, unless this is an impossible task I cannot convince my boss otherwise.
Let's say we have two entities: Item and ItemTranslation.
public class Item
{
public int Id { get; set; }
public string Description { get; set; }
public virtual ICollection<Item> Children { get; set; }
public virtual Item Parent { get; set; }
public virtual ICollection<ItemTranslation> Translations { get; set; }
}
public class ItemTranslation
{
public int Id { get; set; }
public string CultureId { get; set; }
public string Description { get; set; }
public virtual Item Item { get; set; }
}
The requirement is that Item.Description should be filled in based on a language selected by default, but also allowing it to be specified based on what the user wants. The Item.Description column doesn't actually exist in the database.
In SQL this would be easy: all you have to do is query both tables like so
SELECT [Item].[Id], [ItemTranslation].[Description], [Item].[ParentId]
FROM [Item]
LEFT JOIN [ItemTranslation] ON [Item].[Id] = [ItemTranslation].[ItemId]
WHERE [CultureId] = {cultureId}
Or use an OUTER APPLY depending on your implementation. I have added this query to the .FromSql() function built in Entity Framework.
Put this all together in an OData API and this all works fine for one Item. However as soon as you start using $expand (which behind the scenes is a sort of .Include()) it no longer works. The query being sent to the database for the related entities no longer holds the SQL which I specified in .FromSql(). Only the first query does. On top of this when you would query an Item from a different controller e.g. ItemTranslation this would also no longer work since .FromSql() is only applied in the other controller.
I could write a query interceptor which simply replaces the generated SQL by Entity Framework and replaces FROM [Item] with FROM [Item] LEFT JOIN [ItemTranslation] ON [Item].[Id] = [ItemTranslation].[ItemId] WHERE [CultureId] = {cultureId} but I wonder if there is a better implementation than that. Perhaps even a redesign in models. I'm open to suggestions.
FromSql has some limitations. I suspect this is the reason why Include won't work.
But once you use EF, why are you messing with SQL? What difficulties does that query have which prevents you from doing it in LINQ? Left join maybe?
from item in ctx.Items
from itemTranslation in ctx.ItemTranslations.Where(it => it.Item.Id == item.Id).DefaultIfEmpty()
where itemTranslation.CultureId == cultureId
select new { item.Id, itemTranslation.Description, ParentId = item.Parent.Id };
Update
Going over the issue again, I see a further problem. Include will only work on an IQueryable<T> where T is an entity whose navigation properties are mapped properly. Now, from this perspective, it doesn't matter if you use FromSql or LINQ if it produces an IQueryable of some projection instead of an entity, Include won't work for obvious reasons.
To be able to include ItemTranslation entities, your action method should look something like this:
[Queryable]
public IQueryable<Item> GetItems()
{
return db.Items;
}
So the framework can perform $expand on the IQueryable<Item> you return. However, this will include all item translations, not just the ones with the desired culture. If I get it correctly, this is your core issue.
It's obvious as well that you cannot apply this culture filter to an IQueryable<Item>. But you shouldn't do that as this is achieved by $filter in OData:
GET https://.../Items/$expand=Translations&$filter=Translations/CultureId eq culture

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 6 eager loading

I need to load entities from a table into the local cache to display them in a grid control in the application window. The grid control gets its data from the Local property of the DbSet<>. The entities have a collection of subentities which also need to be loaded in advance because lazy loading won't work on the Local entities somehow.
The entity class has all mapped properties virtual and extended change tracking proxies are being used.
Here's what I've found:
myContext.Components.Include(x => x.Parts).ToList();
This is not supported, in fact the compiler throws an error because there is no Include overload that accepts anything else than a string. I have no idea why everybody shows code with expressions here if EF doesn't offer that. So here's what I've done instead:
myContext.Components.Include("Parts").ToList();
Unfortunately this has no effect at all. When I look into the Parts list, it's always empty:
var firstEntity = myContext.Components.Local.First();
var count = firstEntity.Parts.Count;
// count should be > 0 but is = 0
Specifying a different, nonexisting path for the Include method throws an exception at runtime, so EF really does look at that path string and validates it.
After accessing the Parts list, some time later, the parts will be in the list, but that's too late, and I don't know where they come from.
So what's the thing with the expression-parameter Include method, and why doesn't it work at all? What else do I have to consider to use eager loading?
Entity framework 6.1.3 (most other content seems to be for older versions).
Here are the classes:
public class Component
{
[Key]
public virtual int ComponentId { get; set; }
public virtual IList<Part> Parts { get; set; } = new List<Part>();
}
public class Part
{
[Key]
public virtual int PartId { get; set; }
public virtual int ComponentId { get; set; }
public virtual int RequiredCount { get; set; }
}

How to properly make a LINQ query with clauses based on complex calculated data

I have a pretty vast experience with LINQ to Entities and LINQ to SQL but this query is making my head burn.
I need some guidance on how to successfully create a clean LINQ to Entities query, when it has conditional clauses based on calculated columns, and when projecting the data into an object, to also use that calculated columns.
The projected results class is the following:
public class FilterClientsResult
{
public enum ClientType
{
ALL,
LIST,
VIP
}
public int ClientId { get; set; }
public string Document { get; set; }
public string Email { get; set; }
public string LastName { get; set; }
public string Name { get; set; }
public ClientType Type { get; set; }
public int Visits { get; set; }
}
Then the data comes from a primary source and 3 complimentary sources.
The primary source is the Client entity, that among all its properties, it has all the projected properties, with the exception of the last 2 (Type and Visits)
The other sources are:
Subscription, where 1 Client can have many Subscription.
ClientAccess, where 1 Subscription can have many ClientAccess.
GuestListClient, where 1 Client can have many GuestListClient.
So the ClientType Type property is set to FilterClientsResult.ClientType.ALL when that Client is related with both ClientAccess and GuestListClient entities.
It's set to FilterClientsResult.ClientType.VIP when that Client is related only to ClientAccess entities, and FilterClientsResult.ClientType.LIST when that Client is only related to GuestListClient entities.
Then the Visits property is set to the sum of ClientAccess and GuestListClient entities that the Client is related to.
So the query I need should be capable of filtering clients by FilterClientsResult.ClientType but also projecting that FilterClientsResult.ClientType.
And I also need to do filtering by a Visits range, but also projecting that Visits value.
So, what is the optimal way to build a query like that
This works best with query syntax, using the let keyword to store intermediate results as variables:
from c in Clients
let vipCount = c.Subscriptions.SelectMany(s => s.ClientAccesses).Count()
let listCount = c.GuestListClients.Count()
select new FilterClientsResult {
c.ClientId,
...
Type = vipCount > 0 ? listCount > 0 ? ClientType.ALL
: ClientType.VIP
: listCount == 0 ? 0
: ClientType.LIST,
Visits = vipCount + listCount
}
(not syntax checked)
Notice that the ClientType enum must be known to EF, otherwise you first have to project the required properties where Type is an int. Then, after an AsEnumerable(), cast the int to the enum.
Also notice that there is an indecisive value when both counts are 0.

Categories

Resources