18

I'm trying to figure out how to deal with 'Single navigation property case' described in this doc:

Let's say we have 2 models.

class School
{
   public ICollection<Child> Childrens {get; set;}
   ...
}

and

class Child
{
    public int Id {get; set;}
    ...
}

So it's many-to-one relationship created by convention, without explicit foreign key in a Child.

So the question is if we have Child instance and know School.Id is there a way to update this relation without extra call to database to obtain School instance.

silent_coder
  • 6,222
  • 14
  • 47
  • 91
  • 1
    Unless your `Child` has a navigation property/parent id to the parent, you can't do that (that's: w/o a raw query). ORM is about Object Relationships. But w/o loading the parent first, you don't even know if `7352` is a valid parent id, so you'll have to do it at one point anyways or get an hard to parse exception from the database provider when doing `SaveChanges()` – Tseng Jun 02 '17 at 15:52
  • It's unclear what are you trying to achieve - change the parent of an existing child? – Ivan Stoev Jun 02 '17 at 15:58
  • @Tseng Sorry, I'm confused a bit. `7352` it's just a child Id, not parent. And there is many-to-one relation for showing school could have many kids. I assuming that even with fact that I don't have direct `ParentId` inside `Child` EF still create it implicitly for many-to-one relationship. – Ph0en1x Jun 02 '17 at 15:58
  • Then the parent id whatever it is. Doing this in a single query is only possible if `Child` has a navigation property to Parent, or a foreign key to the parent, i.e `ctx.Childs.Add(new Child { ParentId = 5 })` and `ParentId` is a configured principal (or foreign key) for the m:1/1:m relationship, then you could – Tseng Jun 02 '17 at 16:02
  • @Ph0en1x But the **existing** kid should already have parent school, right? You can't create a new kid just by `Id`. The *stub* technique worked in EF6 for adding link to explicit many-to-many relationship, but here that's not the case (and EF Core does not support currently many-to-many with implicit link table). – Ivan Stoev Jun 02 '17 at 16:02
  • @IvanStoev: I think he just wants to perform it in a single DB operation w/o first obtaining the school (which is two operation: 1. read school, 2. add child) – Tseng Jun 02 '17 at 16:05
  • @Tseng Yes, that's correct. I just realized that missed up relation direction. – silent_coder Jun 02 '17 at 16:10
  • @Tseng To perform what - that was the clarification I was asking for. Because in many-to-one relationship with stub entity the only meaningful operation is to change the `SchoolId`. – Ivan Stoev Jun 02 '17 at 16:10
  • @IvanStoev: Well, change `SchoolId` is one, Adding a **new** Child with a single operation is the other meaningful operation – Tseng Jun 02 '17 at 16:12
  • @Tseng Adding **new** Child w/o specifying any property except Id (which will be ignored anyway) and ParentId, really? Anyway, for me the question is unclear. – Ivan Stoev Jun 02 '17 at 16:14
  • @IvanStoev: No a complete new child with **all** properties. Single operation, there is no way around loading at least the child first (or perform a raw update query). But when he has the back property, he can spare loading the school query, so he will still be performing the operation in two queries (read child, update child) instead of 3 (read school, read child, update child) – Tseng Jun 02 '17 at 16:17
  • @Tseng *however this additional request to database to **retrieve** child* sounds like **existing** to me :) – Ivan Stoev Jun 02 '17 at 16:18
  • He can't remove this one, but he can remove school with refactoring of the code. I think his issue is that he has perform 3 queries for a single change operation (which you could do w/o ORM in a single query), he just trying to remove the wrong read query :P – Tseng Jun 02 '17 at 16:20

2 Answers2

22

So the question is if we have Child instance and know School.Id is there a way to update this relation without extra call to database to obtain School instance.

Yes, it's possible. You can create a fake stub School entity instance with Id only, Attach it to the DbContext (this way telling the EF that it is existing), Attach the Child instance for the same reason, and then add the Child to the parent collection and call SaveChanges:

Child child = ...;
var schoolId = ...;

var school = new School { Id = schoolId };
context.Attach(school);
context.Attach(child);
school.Childrens.Add(child);
context.SaveChanges();

Update: Actually there is another cleaner way, since even if the entity has no navigation or FK property, EF Core allows you to access/modify the so called Shadow Properties

Shadow properties are properties that do not exist in your entity class. The value and state of these properties is maintained purely in the Change Tracker.

as soon as you know the name. Which in your case, without configuration would be by convention "SchoolId".

So no fake School entity instance is needed, just make sure the Child is attached and then simply set the shadow property through ChangeTracker API:

context.Attach(child);
context.Entry(child).Property("SchoolId").CurrentValue = schoolId;
context.SaveChanges();
Ivan Stoev
  • 195,425
  • 15
  • 312
  • 343
  • Didn't thought about doing it that way. Personally I think it's not very reliable though. Won't it create a new school in case `schoolId` doesn't exist? And also create all kind of other weird behavior when it's queried later one, because the references for the school will be taken from the tracked cache? – Tseng Jun 02 '17 at 17:02
  • @Tseng Indeed. The caller must be absolutely sure both entities exist. And the `DbContext` should be short lived instance just for this operation. What about reliability, if one of the entities does not exist, the caller will get `DbUpdateException` and no change will be made to the database. Actually the above sequence of operations generates a single `UPDATE` command :) – Ivan Stoev Jun 02 '17 at 17:04
  • Also, you now have a random string in your code for which you won't get any notification if your db field name changes. – Rei Miyasaka Oct 30 '21 at 06:56
3

Based on the updated question

No, there isn't ANY way you could do that by using ORM and strong typing that the ORM offers you, w/o

  • Two-Way Navigation Property
  • At least a ForeignKey/Principal property(SchoolId on Child)
  • Having a shadow foreign key to the parent
  • performing a raw query (which beats the idea of having ORM for strong typing) and being DB agnostic at the same time

    // Bad!! Database specific dialect, no strong typing 
    ctx.Database.ExecuteSqlCommandAsync("UPDATE Childs SET schoolId = {0}", schoolId);
    

When you choose to use an ORM you have to accept certain technical limitations of the ORM framework in question.

If you want to follow Domain Driven Design (DDD) and remove all db specific fields form your entities, it won't be easy to use your domain models as entities.

DDD and ORM don't have very good synergies, there are way better approaches for this, but require a different architectural approach (namely: CQRS+ES (Command Query Responsibility Segregation with Event Sourcing).

This works much better with DDD, since the Events from the EventSourcing are just simple (and immutable) message classes which can be stored as serialized JSON in the database and replayed to reconstruct the domain entity's state. But that's a different story and one could write whole books about this topic.

Old Answer

The above scenario is only possible in a single DB operation, if your Child objects a navigation property/"back reference" to the parent.

class School
{
   public ICollection<Child> Childrens {get; set;}
   ...
}

and

class Child
{
    public int Id {get; set;}
    // this is required if you want do it in a single operation
    public int SchoolId { get; set; }
    // this one is optional
    public School { get; set; }
    ...
}

Then you can do something like:

ctx.Childs.Add(new Child { Id = 7352, SchoolId = 5,  ... });

Of course you first have to know the school Id and know it's valid, otherwise the operation will throw an exception if SchoolId is an invalid value, so I wouldn't recommend this approach.

If you only have the childId and not adding a whole new child you'll still have to get the child first.

// childId = 7352
var child = ctx.Childs.FirstOrDefault(c => c.Id == childId);
// or use ctx.Childs.Find(childId); if there is a chance that 
// some other operation already loaded this child and it's tracked

// schoolId = 5 for example
child.SchoolId = schoolId;
ctx.SaveChanges();
Tseng
  • 61,549
  • 15
  • 193
  • 205
  • `ctx.Childs.Add` will ignore the `Id` and will try to add new `Child` record. – Ivan Stoev Jun 02 '17 at 16:12
  • Yes, it's for adding a new Child. For changing the school, there is no a way around at least retrieving the `Child` object once (with a raw query being the exception, but that beats the purpose of having ORM in the first place). But he can save the `School` query in any case with a parent leading navigation property or foreign key (`SchoolId` here) – Tseng Jun 02 '17 at 16:14
  • https://learn.microsoft.com/en-us/ef/core/modeling/relationships in this doc there is a case described - called 'single navigation property', so in this case there is no explicit `BlogId` inside `Post`, however I'm pretty confident that it's created in a table. So my question was about this case. I thought that there is a way exists, which will change only this "implicit" `BlogId` when adding post to a `Blog.Posts` collection. – silent_coder Jun 02 '17 at 16:19
  • @silent_coder, check again the sample, there is the property BlogId inside Post as well. – E-Bat Jun 02 '17 at 16:23
  • @E-Bat No it's not. Scroll to proper section. – silent_coder Jun 02 '17 at 16:24
  • No, there isn't any other way. There used to be some (3rd party) extension methods for EF 6 which could perform updates w/o reading, dunno if they are available for [EntityFramework.Extended](https://github.com/loresoft/EntityFramework.Extended) where you could do something like `ctx.Childs.Where(c => c.Id == childId).Update(c => new Child { SchoolId = 5}))`, but dunno if they are ported to EF Core, when executed it would generate an "UPDATE. ..." query w/o first loading any entity. But this project doesn't seem to have received any updates in the past 2-3 years, so likely dead – Tseng Jun 02 '17 at 16:25
  • @silent_coder, you refer to the section of shadow property. I would stay away from that because shadow properties are attached to the state of the DbContext, this means they are useless in disconnected scenarios where you send your entity over the wire. – E-Bat Jun 02 '17 at 16:30
  • `BlogId` within `Post` class **isn't a shadow property**. A shadow property is one that's **not defined** on the poco class but exists in the database. Anyways... You **shouldn't EVER** send your EF Entities over the wire (i.e. as api response). Always use ViewModel (or binding model what ever you want to call it for Rest API). Exposing your EF Core Entity model to the WebAPI will kick you in the ass very hard at the same moment, when the database has a slight change and then your api breaks because of additional fields – Tseng Jun 02 '17 at 16:34
  • @Tseng, He is referring to the section where the FK is mapped as shadow property. I know that from an api one should not be returning full entities... But you cannot assume that the domain model will only be consumed from an api, it could be a desktop client as well and there it make sense passing entities from one layer to another, so no shadow porperties for FK is my recomendation. – E-Bat Jun 02 '17 at 16:44
  • 1
    @E-Bat: Well the actual recommendation is not to use the entities for **any** outside of the `DbContext`. Not for domain models, not for webapi, not to be used as Dto and always have separate classes for each of these types. Everything else will end up being messy or Db specific stuff (Id fields) leaking into other layers of your application – Tseng Jun 02 '17 at 16:47
  • @Tseng, not 100% agree, but that topic can be the subject of a very long discussion :) – E-Bat Jun 02 '17 at 16:51
  • what is navigation relation setting for Child.SchoolId ? When i create school and add child and call SaveChanges(), foreign key constraint stop the call with exception, that schoolId is null. – Muflix Jun 06 '19 at 14:53