Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Entity Framework Core: Update relation with Id only without extra call

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.

like image 650
silent_coder Avatar asked Jun 02 '17 15:06

silent_coder


2 Answers

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();
like image 132
Ivan Stoev Avatar answered Nov 20 '22 05:11

Ivan Stoev


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();
like image 3
Tseng Avatar answered Nov 20 '22 05:11

Tseng