Home > Mobile >  How to insert with a foreign key shadow property in EF Core?
How to insert with a foreign key shadow property in EF Core?

Time:04-18

EF Core allows us to omit foreign key properties since the presence of navigation properties is sufficient to establish a relationship between two entities. EF Core will then create what's called a foreign key shadow property on its own under the hood.

I'll use an example similar to the one in the docs:

public class Blog
{
    public int Id { get; set; }
    public string Title { get; set; }

    public List<Post> Posts { get; set; }
}

public class Post
{
    public int Id { get; set; }
    public string Title { get; set; }

    public Blog Blog { get; set; }
}

Now, given a structure like this, where there are no explicit foreign key properties, how would you create a new Post for a Blog with a given ID?

I've tried this:

db.Posts.Add(new Post() {
    Title = "Some title",
    Course = new Blog { Id = 1234 },
});
db.SaveChanges();

But it doesn't seem to work. This seems like a simple problem, but to my surprise, there's no information about it neither in the documentation nor anywhere else I visited.

CodePudding user response:

If you are using Shadow FKs then there are a number of approaches you can use. The first is to fetch the related entity. This means going to the DB, but the benefit is that this asserts that the Blog ID you assume should be valid is valid:

var blog = db.Blogs.Single(x => x.Id == blogId);
var post = new Post
{
    Title = title,
    Blog = blog
};
db.Posts.Add(post);
db.SaveChanges();

As a general rule you shouldn't trust anything coming from a client. If you do want to assume the ID is valid and don't want the round trip then the safe approach is:

var blog = db.Blogs.Local.SingleOrDefault(x => x.Id == blogId);
if (blog == null)
{
    blog = new Blog { Id = blogId };
    db.Attach(blog);
}

var post = new Post
{
    Title = title,
    Blog = blog
};
db.Posts.Add(post);
db.SaveChanges();

This checks the DbContext for a locally cached instance of the Blog (without hitting the DB) and using that if found. If not found, we create one and attach it, then associate that with the Post. We should check the local cache before attaching an instance because if you skip that step and for any reason the DbContext was already tracking a Blog with that ID, the Attach call will fail.

The one caveat of using this approach is that unlike fetching the actual Blog entry, the created Blog is not a complete blog so sending that Post or such anywhere that might expect post.Blog to be a complete blog (such as to get the Name, author, etc.) will not be able to see any details about the blog. This should only be used if you trust the inputs and the entities created don't get referenced beyond creating the required row in the DB.

CodePudding user response:

As mentioned in the beginning of the Shadow and Indexer Properties EF Core documentation topic:

Shadow properties are properties that aren't defined in your .NET entity class but are defined for that entity type in the EF Core model. The value and state of these properties is maintained purely in the Change Tracker.

Hence once you have your entity attached to the context, you can read / write these properties using EF Core metadata and change tracking APIs, as soon as you know their property names.

Which usually you don't, or even if you do, using magic strings is error prone and in general you lose the type safety.

Luckily all needed information is available in EF Core model, so following is a sample helper method for generically setting shadow FK properties:

namespace Microsoft.EntityFrameworkCore
{
    using ChangeTracking;
    using Metadata;

    public static partial class EfCoreChangeTrackingExtensions
    {
        public static void SetForeignKey<TEntity, TRelated>(this ReferenceEntry<TEntity, TRelated> target, params object[] values)
            where TEntity : class
            where TRelated : class
        {
            if (target is null) throw new ArgumentNullException(nameof(target));
            if (values is null) throw new ArgumentNullException(nameof(values));
            var foreignKey = ((INavigation)target.Metadata).ForeignKey;
            if (foreignKey.Properties.Count != values.Length) throw new ArgumentException();
            for (int i = 0; i < values.Length; i  )
                target.EntityEntry.CurrentValues[foreignKey.Properties[i]] = values[i];
        }
    }
}

With sample usage for you scenario:

var postEntry = db.Posts.Add(new Post() {
    Title = "Some title",
});
postEntry.Reference(e => e.Blog).SetForeignKey(1234);
db.SaveChanges();
  • Related