C
C#•4mo ago
engineertdog

EF6 Relationships

I have two classes built in EF Core, that I used to scaffold the database and use in the frontend Blazor app.
c#
public class StreamPointListenerStep {
public Guid Id { get; set; } = Guid.NewGuid();
//....
public StreamPointIntegrationELogger? StreamPointIntegrationELogger { get; set; } = null;
}

public class StreamPointIntegrationELogger {
public Guid Id { get; set; } = Guid.NewGuid();

[ForeignKey(nameof(StreamPointListenerStep))]
public Guid StepId { get; set; }
public StreamPointListenerStep? StreamPointListenerStep { get; set; } = null;
//...
}
c#
public class StreamPointListenerStep {
public Guid Id { get; set; } = Guid.NewGuid();
//....
public StreamPointIntegrationELogger? StreamPointIntegrationELogger { get; set; } = null;
}

public class StreamPointIntegrationELogger {
public Guid Id { get; set; } = Guid.NewGuid();

[ForeignKey(nameof(StreamPointListenerStep))]
public Guid StepId { get; set; }
public StreamPointListenerStep? StreamPointListenerStep { get; set; } = null;
//...
}
Now, building and using this in Blazor is fine. However, I also need the table in a .NET Framework app. So I've ported the DB model to EF6 just to simply use the context for accessing the database and the models without writing the queries manually. However, EF6 refuses to work with this due to the relationship. It throws an error for Because the Dependent Role properties are not the key properties, the upper bound of the multiplicity of the Dependent Role must be '*'. What solutions are there for getting around this? I have to use .NET Framework, and building an API is overhead that's just not needed.
75 Replies
WEIRD FLEX
WEIRD FLEX•4mo ago
if StreamPointIntegrationELogger contains a StreamPointListenerStep then shouldn't StreamPointIntegrationELogger field be an inverse property?
engineertdog
engineertdog•4mo ago
No, the step contains the ELogger I previously had the ELogger ID on the Step, but that caused a FK in both tables which didn't make sense. Thinking about it, the ID should be on Step, and no ID on ELogger. It should be 1-1, so that should fix that then
WEIRD FLEX
WEIRD FLEX•4mo ago
it depends in which direction you need the relationship
engineertdog
engineertdog•4mo ago
A Step has an IntegrationElogger. Step->ELogger, it can't be the other way around And I don't see there being a need to re-share the ELogger, so it should only be 1-1
WEIRD FLEX
WEIRD FLEX•4mo ago
from a technical point of view you place the fk where you want, right? it's more of a matter of what is the meaning of it if you have the fk in the Step then every time you add an "extension" class you have to add an fk column in that class, which means modifying it if you keep the fk in the ELogger class then you don't need to change Step just as an example, there are other cases
engineertdog
engineertdog•4mo ago
Yeah, the FK needs to be on Step And that resolved that issue, but now there's a new one I haven't seen before DbComparisonExpression requires arguments with comparable types. trying to see if it actually gives something useful Nevermind, I was trying to send a string as the Guid
WEIRD FLEX
WEIRD FLEX•4mo ago
if it's a 1-1 it could even be an owned type
engineertdog
engineertdog•4mo ago
One other question if you're still able to help. I want eager loading....but only for properties I specifically tell it to load. I don't understand why they build the system this way
c#
return await dbContext.StreamPointListeners
.Include(s => s.PiCollectivePoint)
.ThenInclude(p => p.PiCollective)
.ToListAsync();
c#
return await dbContext.StreamPointListeners
.Include(s => s.PiCollectivePoint)
.ThenInclude(p => p.PiCollective)
.ToListAsync();
That causes this property Points on PiCollective to be loaded automatically. I don't want that, nor did I specify it. So why in the world is EF loading it?
c#
public IList<PiCollectivePoint> Points { get; set; } = new List<PiCollectivePoint>();
c#
public IList<PiCollectivePoint> Points { get; set; } = new List<PiCollectivePoint>();
tera
tera•4mo ago
if you specified .ThenInclude(p => p.PiCollective) then it will load all of PiCollective's properties. hard to tell what's what without the model. BUT, you should most likely be creating a DTO here since i see you're returning the result somewhere. So project your result to a DTO (.Select(x => new DTO{...} before ToListAsync) and then you don't have to worry about including values.
engineertdog
engineertdog•4mo ago
Yeah, I get the DTO approach. It's just a stupid way to design the system - creating more work for no reason because the tooling isn't sufficient. I'll be able to get around this particular problem by not doing the ThenInclude, and pulling the data ref another, more efficient way. But it's something I'll need to consider.
tera
tera•4mo ago
its usually not a good idea to pass around entities directly anyway. keep them next to the dbcontext they are pulled (tracked) from.. then you cant end up adding them to a different dbcontext or something. keeps your data flow sane. and creating a dto these days is as simple as record Whatever(...) 🙂 not much work
engineertdog
engineertdog•4mo ago
I despise entity tracking and turn that crap off anyway
Nox
Nox•4mo ago
It's not a stupid way to design the system, I don't get what you're talking about You're starting from X, loading Y, and then loading Z And you're complaining that loading Z loads Y?
engineertdog
engineertdog•4mo ago
No, I'm starting from X and loading X. If I want Y, I'll tell EF I want Y, otherwise I don't want it.
Nox
Nox•4mo ago
That's exactly what you're saying with Include
engineertdog
engineertdog•4mo ago
No, because it's including things that I'm not telling it to include.
Nox
Nox•4mo ago
Like what? If I have Library with Books and Authors on each books, and I say ctx.Libraries.Include(x => x.Books).ThenInclude(x => x.Authors), the collection Books will be populated on the Author
engineertdog
engineertdog•4mo ago
So given that example above, that .ThenInclude causes a recursive loop (due to the properties which I'll show). If I wanted an endless loop, I would have told it to give it to me.
Nox
Nox•4mo ago
No. It's not endless. The entities already loaded by the context will be populated.
engineertdog
engineertdog•4mo ago
c#
return await dbContext.StreamPointListeners
.Include(s => s.PiCollectivePoint)
.ThenInclude(p => p.PiCollective)
.ToListAsync();
c#
return await dbContext.StreamPointListeners
.Include(s => s.PiCollectivePoint)
.ThenInclude(p => p.PiCollective)
.ToListAsync();
Given this code, it is called once. It will return a loop of PiCollective with Points which then contain the Collective which then contains the points. I only want 1 level of depth (the depth of which I have explicitly stated)
Nox
Nox•4mo ago
There is only one level of depth.
engineertdog
engineertdog•4mo ago
No, there isn't lol
Nox
Nox•4mo ago
Only one level will be loaded.
engineertdog
engineertdog•4mo ago
Nope. That's why I had to remove the ThenInclude, or tell the JSON parser to ignore recursive loops.
Nox
Nox•4mo ago
Well, all of the levels which are possible with PiCollectivePoint will be loaded Uhhh
engineertdog
engineertdog•4mo ago
Which is stupid. If I wanted that, I would include that in my query.
Nox
Nox•4mo ago
No Your first mistake is JSON Serializing DB entities Your second mistake is assuming there are multiple levels There only needs to be one level for that issue to occur.
engineertdog
engineertdog•4mo ago
I'm not assuming. It's literally in the debugger when you view the results.
c#
public class PiCollectivePoint : IPiCollectivePoint {
//....
public Guid PiCollectiveId { get; set; }
public PiCollective? PiCollective { get; set; } = null;
}

public class PiCollective : IPiCollective {
//...
public IList<PiCollectivePoint> Points { get; set; } = new List<PiCollectivePoint>();
c#
public class PiCollectivePoint : IPiCollectivePoint {
//....
public Guid PiCollectiveId { get; set; }
public PiCollective? PiCollective { get; set; } = null;
}

public class PiCollective : IPiCollective {
//...
public IList<PiCollectivePoint> Points { get; set; } = new List<PiCollectivePoint>();
Nox
Nox•4mo ago
var book = ctx.Books.Include(x => x.Author).First() This will load one book. book.Author - this is populated book.Author.Books.First() - this is populated book.Author.Books.First().Author - this is populated
engineertdog
engineertdog•4mo ago
When I use that .ThenInclude, it populates Points - which I don't ask for, which then populates Points, which populates the Collective, which gets the Points again.
Nox
Nox•4mo ago
It's fulfilling relational navigation on the entities. It's doing its job.
engineertdog
engineertdog•4mo ago
And I don't want it to I want it to do what I told it to do
Nox
Nox•4mo ago
Then don't include Books on Author Or use separate queries. EF's JOB is to hook up C# entities to the results of DB queries.
engineertdog
engineertdog•4mo ago
The other methods for retrieving data make it less efficient and more convoluted.
Nox
Nox•4mo ago
You're asking for it to not do the thing people use it for. Your misunderstanding of the tool does not mean it's shit, or that "it's not doing what I tell it to"
engineertdog
engineertdog•4mo ago
No description
engineertdog
engineertdog•4mo ago
The purpose is to reduce work, not introduce work.
Nox
Nox•4mo ago
It introduces work for those who don't bother understanding what it does.
Nox
Nox•4mo ago
Within the top half of the page
Nox
Nox•4mo ago
No description
Nox
Nox•4mo ago
Less than 15 seconds of my time to find this information Your mistake is serializing a DB entity which can naturally have cyclical relationships. This is not EF's fault, this is not JSON's fault, it is yours.
engineertdog
engineertdog•4mo ago
Converting something like this to explicit loading just doesn't make sense, especially where they don't even support loading lists in bulk.
No description
Nox
Nox•4mo ago
I won't even comment on the absurdity of that query, but "loading lists in bulk"?
engineertdog
engineertdog•4mo ago
Exactly, and yes, all that data is required for a process to operate correctly. So according to what I found, if I wanted to query those records using .ToList, but using the explicit method, I’d also have to introduce a loop to explicitly load the paths. And even then, the official documentation is broken and doesn’t work.
Nox
Nox•4mo ago
The official documentation is not broken, your understanding or usage is likely what's broken. There would be no loop needed. Using a loop to load a series of related data is obviously highly inefficient
Patrick
Patrick•4mo ago
It's absolutely not a stupid way to build a system. Your queries are equivalent of doing SELECT * FROM Foos. Your queries are heavy and inefficient, that's what's stupid. Anyone who queries SQL in a typical way would do SELECT Bar, Baz, Qaz FROM Foos. Throwing AsNoTracking on there and including everything is an anti pattern. Blaming the tooling for being wrong, when you're not using it in its intended way is not a resolution.
engineertdog
engineertdog•4mo ago
Not once has anyone provided documentation showing the “proper” way to do what’s needed. And just because you might not need all data from certain relationships, doesn’t mean there aren’t cases for processes that do. That being said, if the documentation on explicit loading isn’t broken, then why does VS throw an error for using the exact pattern as documented? Have you tried it yourself like I have? There’s a GitHub issue on the usage of selecting a list of objects from the DB using that method because it’s not supported.
Patrick
Patrick•4mo ago
then why does VS throw an error for using the exact pattern as documented
because it's your code that is the problem 🙂
Patrick
Patrick•4mo ago
I don't understand your message. What you're doing isn't legal.
engineertdog
engineertdog•4mo ago
It's literally in the documentation I linked lol
engineertdog
engineertdog•4mo ago
No description
Patrick
Patrick•4mo ago
Your code isn't 1:1 with the documentation ,then 🙂
engineertdog
engineertdog•4mo ago
Documentation isn't accurate Not the first time a vendor has inaccurate docs, but it's fine
Patrick
Patrick•4mo ago
Hard to say when you don't actually post your code
engineertdog
engineertdog•4mo ago
I posted the model above earlier between those entities
Patrick
Patrick•4mo ago
you've posted irrelevant snippets
engineertdog
engineertdog•4mo ago
You want all 10 classes, or just the two classes with the relevant paths?
Patrick
Patrick•4mo ago
whatever type StreamPointListeners is and its members
engineertdog
engineertdog•4mo ago
c#
public class StreamPointListener {
public Guid Id { get; set; } = Guid.NewGuid();
public required string Name { get; set; }
public required string Description { get; set; }
public bool Enabled { get; set; } = true;
public Guid? StreamAgentId { get; set; } = null;
public StreamAgent? StreamAgent { get; set; } = null;
public bool StickToAgent { get; set; } = false;
public Guid PiCollectivePointId { get; set; }
public virtual PiCollectivePoint? PiCollectivePoint { get; set; } = null;
public StreamDataType StreamDataType { get; set; }
public bool LogValueOnly { get; set; } = false;
public IList<StreamPointListenerStep> Steps { get; set; } = new List<StreamPointListenerStep>();
public DateTime? LastUpdate { get; set; } = null;
public float? ValuesPerMinute { get; set; } = null;
}
c#
public class StreamPointListener {
public Guid Id { get; set; } = Guid.NewGuid();
public required string Name { get; set; }
public required string Description { get; set; }
public bool Enabled { get; set; } = true;
public Guid? StreamAgentId { get; set; } = null;
public StreamAgent? StreamAgent { get; set; } = null;
public bool StickToAgent { get; set; } = false;
public Guid PiCollectivePointId { get; set; }
public virtual PiCollectivePoint? PiCollectivePoint { get; set; } = null;
public StreamDataType StreamDataType { get; set; }
public bool LogValueOnly { get; set; } = false;
public IList<StreamPointListenerStep> Steps { get; set; } = new List<StreamPointListenerStep>();
public DateTime? LastUpdate { get; set; } = null;
public float? ValuesPerMinute { get; set; } = null;
}
Patrick
Patrick•4mo ago
what a surprise
public virtual PiCollectivePoint? PiCollectivePoint { get; set; } = null;
public virtual PiCollectivePoint? PiCollectivePoint { get; set; } = null;
engineertdog
engineertdog•4mo ago
c#
public class PiCollectivePoint : IPiCollectivePoint {
public Guid Id { get; set; } = Guid.NewGuid();
public DateTime CollectionDate { get; set; } = DateTime.UtcNow;
public Guid PiCollectiveId { get; set; }
public PiCollective? PiCollective { get; set; } = null;
public required string PointName { get; set; }
}
c#
public class PiCollectivePoint : IPiCollectivePoint {
public Guid Id { get; set; } = Guid.NewGuid();
public DateTime CollectionDate { get; set; } = DateTime.UtcNow;
public Guid PiCollectiveId { get; set; }
public PiCollective? PiCollective { get; set; } = null;
public required string PointName { get; set; }
}
Patrick
Patrick•4mo ago
your code isn't right 🙂
engineertdog
engineertdog•4mo ago
Actually, that was added after the code testing last night. I never include those virtuals lol
Patrick
Patrick•4mo ago
Collection is used for a collection, not a singular nav prop so the document is accurate, your code is just wrong
engineertdog
engineertdog•4mo ago
But for the second document you linked for making more efficient queries, if I didn't need columns, then I wouldn't have the columns in the first place. They're needed for a reason in 99% of scenarios.
Patrick
Patrick•4mo ago
that doesn't make logical sense you own a house, you don't use every room of the house every day, all day not every piece of code requires every single column from an entity
engineertdog
engineertdog•4mo ago
They're used in their respective processes. The processes need the data to run. No, not every piece of code does. Only the relevant classes do. And those classes are the only ones that access the data anyway.
Patrick
Patrick•4mo ago
you're literally arguing against industry specialists the fact you've put AsNoTracking on your queries is enough of an indicator that you're bastardising to do something you can do in a much better way
engineertdog
engineertdog•4mo ago
Again, no. Hard tracking the models sucks.
Patrick
Patrick•4mo ago
you've repeatedly said the documentation is wrong, when I've just proven it's your code, said the tooling is inadequate, when it's your practices and now argue that the practices championed all across the industry are wrong. i have no interest in continuing this conversation. good luck.
engineertdog
engineertdog•4mo ago
The model is either getting updated or used. There's no inbetween where all the columns aren't required.