C
C#4mo ago
Utsuhoagie

EF Core with TPC hierarchy: InvalidCastException despite no casting

I have a problem when trying to use TPC hierarchy mapping in EF Core 8, where I get InvalidCastExceptions despite never casting anything. What I wanted to do is have a BaseAuditedEntity class for most models in my app, so that I could use it for a feature: TierList entity has many TierListItems, and each TierListItem relates to a BaseAuditedEntity entity in the app, such as a Circle (music group) or ArrangementSong or any other base entity. The (simplified) models:
public class Circle : BaseAuditedEntity
{
public string Name { get; set; }

public List<ArrangementSong> ArrangementSongs { get; set; }
}

public class ArrangementSong : BaseAuditedEntity
{
public string Title { get; set; }

public int CircleId { get; set; }
public Circle Circle { get; set; }

public List<OfficialSong> OfficialSongs { get; set; }
public List<OfficialSongArrangementSong> OfficialSongArrangementSongs { get; set; }
}

public class OfficialSong : BaseAuditedEntity
{
public string Title { get; set; }

public required List<ArrangementSong> ArrangementSongs { get; set; }
public required List<OfficialSongArrangementSong> OfficialSongArrangementSongs { get; set; }
}

public class TierListItem : BaseEntity
{
public int TierListTierId { get; set; }
public TierListTier TierListTier { get; set; } = default!;

public int SourceId { get; set; }
public BaseAuditedEntity Source { get; set; }
}

public abstract class BaseAuditedEntity : BaseEntity
{
public DateTime CreatedOn { get; set; }
public string CreatedByUserName { get; set; } = string.Empty;

public DateTime? UpdatedOn { get; set; }
public string? UpdatedByUserName { get; set; }
}

public abstract class BaseEntity
{
public int Id { get; set; }
}
public class Circle : BaseAuditedEntity
{
public string Name { get; set; }

public List<ArrangementSong> ArrangementSongs { get; set; }
}

public class ArrangementSong : BaseAuditedEntity
{
public string Title { get; set; }

public int CircleId { get; set; }
public Circle Circle { get; set; }

public List<OfficialSong> OfficialSongs { get; set; }
public List<OfficialSongArrangementSong> OfficialSongArrangementSongs { get; set; }
}

public class OfficialSong : BaseAuditedEntity
{
public string Title { get; set; }

public required List<ArrangementSong> ArrangementSongs { get; set; }
public required List<OfficialSongArrangementSong> OfficialSongArrangementSongs { get; set; }
}

public class TierListItem : BaseEntity
{
public int TierListTierId { get; set; }
public TierListTier TierListTier { get; set; } = default!;

public int SourceId { get; set; }
public BaseAuditedEntity Source { get; set; }
}

public abstract class BaseAuditedEntity : BaseEntity
{
public DateTime CreatedOn { get; set; }
public string CreatedByUserName { get; set; } = string.Empty;

public DateTime? UpdatedOn { get; set; }
public string? UpdatedByUserName { get; set; }
}

public abstract class BaseEntity
{
public int Id { get; set; }
}
The base classes configuration:
modelBuilder.Entity<BaseEntity>()
.UseTpcMappingStrategy();


modelBuilder.Entity<Circle>()
.HasMany(c => c.ArrangementSongs)
.WithOne(a => a.Circle)
.IsRequired();
// ...
modelBuilder.Entity<BaseEntity>()
.UseTpcMappingStrategy();


modelBuilder.Entity<Circle>()
.HasMany(c => c.ArrangementSongs)
.WithOne(a => a.Circle)
.IsRequired();
// ...
A simple query could be like this:
var circles_Res = await _context.Circles
.Include(c => c.ArrangementSongs)
.Where(c => query.searchName == null || EF.Functions.ILike(c.Name, $"%{query.searchName}%"))
.Select(c => new CircleResponse(c)
{
ArrangementSongTitles = c.ArrangementSongs.Select(a => a.Title).ToList(),
})
.ToListAsync();
var circles_Res = await _context.Circles
.Include(c => c.ArrangementSongs)
.Where(c => query.searchName == null || EF.Functions.ILike(c.Name, $"%{query.searchName}%"))
.Select(c => new CircleResponse(c)
{
ArrangementSongTitles = c.ArrangementSongs.Select(a => a.Title).ToList(),
})
.ToListAsync();
But when running, I get this exception (it doesn't happen if I don't .Include(a => a.Circle) ) in the logs. I'm guessing that this is a problem with BaseEntity rows in the DB. There is no BaseEntity table because it's TPC, but after a migration I made to update the models from standalone Ids to using a base class, then I see that they use a common sequence. However, the existing Ids are unchanged, so there are many rows that share the same Ids. But I'm not sure if this is actually the cause, or how to fix this. Any help is greatly appreciated!
7 Replies
Unknown User
Unknown User4mo ago
Message Not Public
Sign In & Join Server To View
Omnissiah
Omnissiah4mo ago
table per concrete type isn't TierListItem "outside" the circle/song domain? in the end there isn't actually a relation
Utsuhoagie
UtsuhoagieOP4mo ago
TierListItem is simply any kind of item that can be made into tier lists. For example you could make tier lists for songs, circles (music groups), remixes, games, characters,... So I wanted TierListItem to have a relation to a BaseAuditedEntity, because most entities can be a tier list item What I planned is to allow the user to navigate to the "source" of a TierListItem by clicking on it, which would require the type + PK (in this case just an ID) Also, I currently have 1 but potentially a few more methods on BaseEntity which returns a formatted label string depending on the derived class, so I'm thinking to keep this hierarchy
Omnissiah
Omnissiah4mo ago
I'm guessing that this is a problem with BaseEntity
apart from the fact that i would give a specific id field to every type and not use inheritance for that (maybe i could use interface), i'm surprised that ef core doesn't ask to make BaseAuditedEntity a concrete type
Utsuhoagie
UtsuhoagieOP4mo ago
I mean, that's exactly what I did before. When I started working on this feature, I realized TierListItem could be related to basically any other entity in the app, and so I made all of them inherit from BaseEntity (or audited) I could simply not use any inheritance and just manually have nullable navigations for each entity type instead (there's just gonna be like 8 max anyway), but I wanted to try doing something I've never done before
Omnissiah
Omnissiah4mo ago
if you want to keep stuff so "dynamic" i think you will have more problems, or maybe not problems but more complexity if you already know that this is a closed set of objects then what i would do is add collection/navigation property for each one of them in the TierListItem maybe i should think this throught a little bit more but i'm fried
Utsuhoagie
UtsuhoagieOP4mo ago
Perhaps you're right. It's really just a small project I'm practicing, nothing serious. At most I would expect about 8 different entity types, but currently it's just about 4 But still, I want to figure out what causes the issue, I really don't understand why it's trying to cast objects that aren't related, and I'm not casting anything My best guess is that many entities have the same int IDs because they used to be separate and EF Core itself is trying to cast them randomly to other types
Want results from more Discord servers?
Add your server