ef7 unable to retrieve child collections' child object's properties - c#

EF7 doesn't support lazy loading of child objects, but does support the .Include() function. That being said, I'm struggling with something and am not sure if it just isn't possible in EF7, or I've just been staring at this too long.
Suppose something like the following (checking reg.Activities.Task.Ordinal (an int), Task is always null, even when I check the DB myself and am certain there is in fact a related record)...
public void SomeOtherMethod()
var r = getRegistration(User.UserName);
var act = r.Activities
.Where(a => a.IsDone == false) // unfinished
.OrderByDescending(o => o.Task.Ordinal) // Task indicates activity type, is always null
.FirstOrDefault(); // to get a user's most recent unfinished activity
//DO SOMETHING WITH ACT
}
public Registration getRegistration(string userName) {
var reg = _context.Registrations
.Where(r => r.User.UserName == userName) // this works however?
.Include(r => r.Acvitities) // List<Activity>
.FirstOrDefault();
return reg;
}
...I have the navigation properties in place in the model classes, but .Task above is null and not loaded.
Further, since the query has been projected, I can't .Include additional properties any more in the creation of act. I cant .ThenInclude in the creation of reg because class Registration does not include a definition for a Task property (but Registration does have a collection of Activities that are List<Activity>, and Activity does have a Task that is related to another table/class that defines the tasks and the order they should be presented to users per Activity.
I've tried various incantations of .Join, .Include and .ThenInclude hoping to be able to join the Task to each of the Activities while returning the Registration object, but this fails because Registration itself does not contain a Task property.
I considered creating a new issue on GitHub, but am not yet certain that it isn't very doable and I'm just not looking at this correctly.
UPDATE1: Mihail suggested using...
.Include(r => r.Activities.Select(resp => resp.Responses))
...but this yields an exception. This SO (https://stackoverflow.com/a/30151601/3246805) indicates thats for EF5 and that .ThenInclude should be used.
However, trying that suggestion...
.ThenInclude(r => r.Select(t => t.Task))
...yields the following Exception...
The properties expression 'r => {from Activity t in r select [t].Task}' is not valid. The expression should represent a property access: 't => t.MyProperty'. When specifying multiple properties use an anonymous type: 't => new { t.MyProperty1, t.MyProperty2 }'.
Parameter name: propertyAccessExpression
UPDATE2: Stafford asked for schema. Best effort at a sharable repo...
public class RegistrationData {
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
[Required]
public MyUser User { get; set; } // MyUser : IdentityUser
//blah blah, more fields
public List<UserTask> Activitys { get; set; }
}
public class UserTask {
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
[Required]
public bool IsDone { get; set; } = false;
[Required]
public int RegistrationId { get; set; }
[Required]
public RegistrationData Registration { get; set; }
[Required]
public int TaskId { get; set; }
[Required]
public Task Task { get; set; }
public List<UserResponse> Responses { get; set; }
}
public class Task {
[Required]
[DatabaseGenerated(DatabaseGeneratedOption.None)] // ID comes from loaded config
public int Id { get; set; }
[StringLength(20, MinimumLength = 1)]
public string Name { get; set; }
[Required]
public int Ordinal { get; set; }
[Required]
public int GroupId { get; set; }
}
public class UserResponse {
[Required]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
[Required]
public int UserTaskId { get; set; }
[Required]
public int QuestionNumber { get; set; }
}

Use Include followed by ThenInclude for the child's child properties. The child properties may not show up in intellisense for ThenInclude, but just enter it anyway - it will compile and operate as expected.
var reg = _context.Registrations
.Where(r => r.User.UserName == userName)
.Include(r => r.Acvitities).ThenInclude(a => a.Task)
.Include(r => r.Activities).ThenInclude(a => a.SomethingElse)
.FirstOrDefault();
return reg;

Related

The instance of entity type X cannot be tracked because another instance with the key value '{Id:}' is already being tracked

i am using entity framework core 7 in my .net 7 project. Here is my update method
public async Task UpdateAsync(int id, SiteDto dto)
{
Site site = await _context.Sites.Where(x => x.Id == id)
.Include(x => x.Network)
.Include (x => x.Centre)
.Include(x => x.SiteDayParts).ThenInclude(x => x.SiteFrames)
.Include(x => x.Resolution)
.SingleOrDefaultAsync();
_mapper.Map(dto, site);
await _context.SaveChangesAsync();
}
SiteDto object, context has the same fields
public class SiteDto
{
public string Name { get; set; }
public string Address { get; set; }
public MediaFormat Formats { get; set; }
public int ResolutionId { get; set; }
public List<SiteDayPartDto> SiteDayParts { get; set; }
}
public class SiteDayPartDto
{
public int Id { get; set; }
public TimeSpan StartTime { get; set; }
public TimeSpan EndTime { get; set; }
public int Duration { get; set; }
public List<SiteFrameDto> SiteFrames { get; set; }
}
public class SiteFrameDto
{
public int Id { get; set; }
public int FrameId { get; set; }
}
Mapping config
CreateMap<SiteDto, Site>();
CreateMap<SiteDayPartDto, SiteDayPart>()
.ForMember(dest => dest.AdLength, opt => opt.MapFrom(x => x.Duration));
CreateMap<SiteFrameDto, SiteFrame>()
When I trying to save changes, I get an error "The instance of entity type 'SiteFrame' cannot be tracked because another instance with the key value '{Id: 1}' is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached."
Database - Postgresql.
Context registered as scoped, mapper, as I know, don`t create new object.
It is interesting that, if I use entity framework 6, it works well.
I have solved the problem like this. First, install Automapper.Collections and add:
services.AddAutoMapper((serviceProvider, automapper) =>
{
automapper.AddCollectionMappers();
}, Assembly.GetExecutingAssembly());
Then if you have nested list with a nested list, as in my example, add this mapping:
CreateMap<SiteDayPartDto, SiteDayPart>()
.EqualityComparison((odto, o) => odto.Id == o.Id)
CreateMap<SiteFrameDto, SiteFrame>()
.EqualityComparison((odto, o) => odto.Id == o.Id)
EqualityComparison will check if entity contains dto or not (it may be useful https://github.com/AutoMapper/Automapper.Collection). Good luck!

.Net Core Entity Framework .Include Eager load does not load entity

My first question - be kind :-).
In the code below, I am attempting to reference an "Include(d)" entity (Schedules) to obtain its Name property. In EF 6, both "schedule" and "schedule_2" return the correct value of Name. In EF Core, "schedule" returns NULL and "schedule_2" returns the correct value of Name.
I do not understand why I should have to load the "schedules" List. Shouldn't the .Include force an Eager Load of the Schedules for each Election such that each Election Schedule's Name property would be available for the "schedule" assignment?
// Relevant Model entities in database
// DbSet<Election> Elections { get; set; }
//
// The following are the related classes defined in the database context...
public class Election
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<Schedule> Schedules { get; set; }
}
public class Schedule
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public int Id { get; set; }
public int? CfsElectionId { get; set; }
public string Name { get; set; }
[Required] // sets cascade delete
[ForeignKey("CFSElectionID")]
public virtual Election Election { get; set; }
}
class Program
{
static void Main()
{
var db = new FfmsDbContext();
var elections = db.Elections
.Include(i => i.Schedules)
.ToList();
//The following returns NULL?
var schedule = elections.First().Schedules?.First().Name ?? "NULL";
var schedules = db.Schedules
.ToList();
//The following returns the correct Name property?
var schedule_2 = elections.First().Schedules?.First().Name ?? "NULL";
Console.WriteLine($#"sched: {schedule}");
Console.WriteLine($#"schedules.First().Name: {schedules.First().Name}");
Console.WriteLine($#"sched2: {schedule_2}");
Console.WriteLine("Done...");
Console.ReadLine();
}
}
/*
Output...
sched: NULL
schedules.First().Name: Candidates
sched2: Candidates
Done...
*/
Turns out that my problem ended up being in the References of the Class.
I had accidentally chosen System.Data.Entity as the offered choice for .Include.
The correct reference should have been Microsoft.EntityFrameworkCore.
Once I adjusted the reference, the .Include worked as desired.

Entity Framework Core (Postgres) Multiple Includes creates ghost property

I'm having an issue with a series of Include/ThenInclude in a query.
Here is my EntityFrameworkCore Query :
var fund = await funds.Where(x => x.Id == fundId)
.Include(f => f.Compositions.Where(compo => compo.Date == compositionDate))
.ThenInclude(c => c.CompositionItems)
.ThenInclude(item => item.Asset)
.FirstOrDefaultAsync(token)
?? throw new NotFoundException(nameof(Fund), fundId);
I recieve a 'CompositionDate does not exists' error.
As you can see the CompositionDate property is at the Compositions Level.
When I check the SQL generated I get this in a subquery Select statement :
SELECT f1."CompositionFundId", f1."CompositionDate", f1."AssetId", f1."Amount", a."Id", a."CountryCode", a."Currency", a."FundCompositionDate", a."FundCompositionFundId", a."Isin", a."Name", a."SecurityType", a."Ticker", a."Coupon", a."GicsSector", a."InvestmentCase", a."IpoDate", a."Theme"
FROM "FundCompositionItem" AS f1
INNER JOIN "Asset" AS a ON f1."AssetId" = a."Id"
Those 2 properties a."FundCompositionDate", a."FundCompositionFundId" doesn't exists at the 'Asset' level.
They exists in the parent (at the 'Where' level on the first Include).
I'm using Postgres provider for EFcore. Could this be the issue?
Should I be using the select anonymous type .Select(x => new { Fund = x, Compo = x.Compo.Where(...), etc... }?
I would like to preserve the navigation properties if possible. (accessing assets from compositionItems)
Any help would be much appreciated.
Edit:
Models as requested by Atiyar:
public class Portfolio : AuditableEntity
{
public Guid Id { get; set; }
public Guid Name{ get; set; }
}
public class Fund : Portfolio
{
// Irrelevant properties
public IList<FundComposition> Compositions { get; } = new List<FundComposition>();
}
public class FundComposition
{
public Fund Fund { get; set; }
// Primary key / Foreign key
public Guid FundId { get; set; }
// Primary Key
public DateTime Date { get; set; }
public List<FundCompositionItem> CompositionItems { get; set; } = new();
}
public class FundCompositionItem
{
public FundComposition Composition { get; set; }
// Primary Key
public Guid CompositionFundId { get; set; }
// Primary Key
public DateTime CompositionDate { get; set; }
public Asset Asset { get; set; }
// Primary Key
public Guid AssetId { get; set; }
public double Amount { get; set; }
}
public class Asset : BaseEntity
{
// Primary Key
public Guid Id { get; set; }
public string Name { get; set; }
public string Currency { get; set; }
// more properties
}
In my experience, I've applied the Include() and ThenInclude() first and then applied the any conditional clauses afterwards. I'm also not sure if using Where inside of an include method does what you expect it to.
You can also apply your conditional in the first parameter of .FirstOrDefaultAsync().
var fund = await funds.Where(x => x.Id == fundId)
.Include(f => f.Compositions)
.ThenInclude(c => c.CompositionItems)
.ThenInclude(item => item.Asset)
.FirstOrDefaultAsync(x =>
x.Id == fundId && x.Compositions.Any(compo => compo.Date == compositionDate),
token
)

Lookup-able models

I'm currently working on a project using MongoDB as it's database and I got some issues on how to solve the problem of having a model containing some ids to foreign documents and looking them up without needing to fall back to BsonDocument.
Originally I had this (stripped down) model class:
public class TestSession
{
[BsonId]
public ObjectId Id { get; set; }
[BsonElement("taskId")]
public ObjectId? TaskId { get; set; }
}
Sometimes I need the concrete TaskConfiguration referenced by TaskId.
In this case I use a $lookup to resolve it.
First I introduced a new member to hold the concrete TaskConfiguration instance changing my model class to:
public class TestSession
{
[BsonId]
public ObjectId Id { get; set; }
[BsonElement("taskId")]
public ObjectId? TaskId { get; set; }
[BsonIgnore]
public TaskConfiguration Task { get; set; }
}
Then I realized that the aggregation was unable to deserialize the looked-up and unwound Task value, I guess because of BsonIgnore.
I could not find any options to override this and I couldn't come up with any other solution to this.
Finally I decided to split the model class into the initial one containing exactly the database representation of the document and a derived class containing the resolved Task member without the BsonIgnore attribute in order for it to be deserializable.
The resulting classes look like this:
public class TestSessionModel
{
[BsonId]
public ObjectId Id { get; set; }
[BsonElement("taskId")]
public ObjectId? TaskId { get; set; }
}
public class TestSession : TestSessionModel
{
[BsonElement("task")]
public TaskConfiguration Task { get; set; }
}
Although this approach seems to work I now have to split up all similiar classes which not only creates a lot of classes potentially but also could potentially confuse the reader what class to use.
Additionally one has to be careful to not store the derived classes into the database.
My question now is if my current approach is actually the way to go in this case or if there are better/safer alternatives?
you can do it with the BsonIgnore approach but you need to project the final result like this:
var res = collection.AsQueryable()
.Where(t => t.Id == session.Id)
.Join(
foreignColl.AsQueryable(), // foreign collection
s => s.TaskId, // local field
c => c.Id, // foreign field
(s, c) => new TestSession // projection
{
Id = s.Id,
Name = s.Name,
TaskId = s.TaskId,
Task = c
})
.Single();
here's the full program i used for testing:
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Entities;
using MongoDB.Entities.Core;
using System.Linq;
namespace StackOverflow
{
public class TestSession : Entity
{
public string Name { get; set; }
[BsonRepresentation(BsonType.ObjectId)]
public string TaskID { get; set; }
[BsonIgnore]
public TaskConfiguration Task { get; set; }
}
public class TaskConfiguration : Entity
{
public int NumOfIterations { get; set; }
}
public static class Program
{
private static void Main()
{
new DB("test");
var task = new TaskConfiguration { NumOfIterations = 10 };
task.Save();
var session = new TestSession { Name = "This is a test session", TaskID = task.ID };
session.Save();
var res = DB.Queryable<TestSession>()
.Where(t => t.ID == session.ID)
.Join(
DB.Queryable<TaskConfiguration>(),
s => s.TaskID,
c => c.ID,
(s, c) => new TestSession
{
ID = s.ID,
Name = s.Name,
TaskID = s.TaskID,
Task = c
})
.Single();
}
}
}

How to hide items, in an API reply, from a db query?

I'm currently using MVC with EF to have a small server with API querying a SQL database. But in the API reply I'm not able to hide some parameters.
The main object
public class AssetItem
{
[Key]
public Int32 AssetId { get; set; }
public String AssetName { get; set; }
public int OdForeignKey { get; set; }
[ForeignKey("OdForeignKey")]
public OperationalDataItem OperationalDataItem { get; set; }
}
The other one:
public class OperationalDataItem
{
[Key]
public Int32 OperationalDataId { get; set; }
public String Comunity { get; set; }
public List<AssetItem> AssetItems { get; set; }
}
From what I have read, this should be ok, I have also set the context:
public AssetContext(DbContextOptions<AssetContext> options) : base(options)
{}
public DbSet<AssetItem> AssetItems { get; set; }
public DbSet<OperationalDataItem> OperationalDataItems { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<AssetItem>().HasOne(p =>
p.OperationalDataItem).WithMany(b => b.AssetItems).HasForeignKey(p =>
p.OdForeignKey);
}
And the seeding in program.cs
context.AssetItems.Add(
new AssetItem { AssetName = "Test test", OdForeignKey = 1,
OperationalDataItem =
new OperationalDataItem {Comunity = "Comunity1" }});
So calling the API this results in:
{ "assetId":3,
"assetName":"Test test",
"odForeignKey":1,
"operationalDataItem":null }
From what I read this is because of the lazy loading, how can I hide the result operationalDataItem?
In case is not possible i have of course try to query for it and give it back and it give something like:
{ "assetId":3,
"assetName":"Test test",
"odForeignKey":1,
"operationalDataItem":
{ "operationalDataId":1,
"comunity":"Comunity1",
"assetItems":[
But in this case I would like to hide "assetsItems" in the reply to the FE.
How can I hide those parameters?
The API is quite simple, just an example code:
var todoItem = await _context.AssetItems.FindAsync((Int32)id);
var item = _context.OperationalDataItems.Find((Int32)todoItem.OdForeignKey);
todoItem.OperationalDataItem = item;
return todoItem
If you want to fetch data from the database, but you only want to fetch some properties, use Select. Usually this is more efficient than using Find, because you'll only transfer the data that you actually plan to use.
To fetch some properties of the assetItem that has primary key assetItemId:
var result = dbContext.AssetItems
.Where(assetItem => assetItem.AssetItmId = assetItemId)
.Select(assetItem => new
{
// Select only the properties that you plan to use
Id = assetItem.AssertItemId,
Name = assetItem.Name,
OperationalData = new
{
// again, select only the properties that you plan to use
Id = assetItem.OperationalData.OperationalDataId,
Community = assetItem.OperationalData.Community,
},
})
.FirstOrDefault();
Or the other way round:
Fetch several properties of all (or some) OperationalDataItems, each with some properties of all (or some) of its AssetItems:
var result = dbContext.OperqationalDataItems
.Where(operationalDataItem => ...) // only if you don't want all
.Select(operationalDataItem => new
{
Id = operationalDataItem.Id,
Community = operationalDataItem.Community
AssetItems = operationalDataItem.AssetItems
.Where(assetItem => ...) // only if you don't want all its assetItems
.Select(assetItem => new
{
// Select only the properties you plan to use:
Id = assetItem.Id,
...
// not useful: you know the value of the foreign key:
// OperationalDataId = assetItem.OperationalDataId,
})
.ToList();
})
.ToList(); // or: FirstOrDefault if you expect only one element
Entity framework knows your one-to-many relation and is smart enough to know which (group-)join is needed for your query.
Some side remarks
You've declare your many-relation a List<AssetItem>. Are you sure that operationalDataItem.AssetItems[4] has a defined meaning? Wouldn't it be better to stick to the entity framework code first conventions? This would also eliminate the need for most attributes and / or fluent API
public class OperationalDataItem
{
public int Id { get; set; }
public String Comunity { get; set; }
...
// Every OperationalDataItem has zero or more AssetItems (one-to-many)
public virtual ICollection<AssetItem> AssetItems { get; set; }
}
public class AssetItem
{
public int Id { get; set; }
public String Name { get; set; }
...
// every AssetItem belongs to exactly one OperationalDataItem, using foreign key
public int OperationDataItemId { get; set; }
public virtual OperationalDataItem OperationalDataItem { get; set; }
}
In entity framework the columns of a table are represented by the non-virtual properties. The virtual properties represent the relations between the tables (one-to-many, many-to-many)
Because I stuck to the conventions, no attributes nor fluent API is needed. Entity framework is able to detect the one-to-many relation and the primary and foreign keys. Only if I am not satisfied with the names or the types of the columns I would need fluent API.

Categories

Resources