How to define navigation for in-class encapsulated property?
It's possible, but the intermediate class must be mapped as fake entity, serving as principal of the one-to-many relationship and being dependent of one-to-one relationship with the actual principal.
Owned entity type looks a good candidate, but due to EF Core limitation of not allowing owned entity type to be a principal, it has to be configured as regular "entity" sharing the same table with the "owner" (the so called table splitting) and shadow "PK" / "FK" property implementing the so called shared primary key association.
Since the intermediate "entity" and "relationship" with owner are handled with shadow properties, none of the involved model classes needs modification.
Following is the fluent configuration for the sample model
modelBuilder.Entity<Posts>(entity =>
{
// Table splitting
entity.ToTable("Blogs");
// Shadow PK
entity.Property<int>(nameof(Blog.Id));
entity.HasKey(nameof(Blog.Id));
// Ownership
entity.HasOne<Blog>()
.WithOne(related => related.Posts)
.HasForeignKey<Posts>(nameof(Blog.Id));
// Relationship
entity
.HasMany(posts => posts.PostsCollection)
.WithOne()
.HasForeignKey(related => related.BlogId);
});
The name of the shadow PK/FK property could be anything, but you need to know the owner table name/schema and PK property name and type. All that information is available from EF Core model metadata, so the safer and reusable configuration can be extracted to a custom extension method like this (EF Core 3.0+, could be adjusted for 2.x)
namespace Microsoft.EntityFrameworkCore
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using Metadata.Builders;
public static class CustomEntityTypeBuilderExtensions
{
public static CollectionNavigationBuilder<TContainer, TRelated> HasMany<TEntity, TContainer, TRelated>(
this EntityTypeBuilder<TEntity> entityTypeBuilder,
Expression<Func<TEntity, TContainer>> containerProperty,
Expression<Func<TContainer, IEnumerable<TRelated>>> collectionProperty)
where TEntity : class where TContainer : class where TRelated : class
{
var entityType = entityTypeBuilder.Metadata;
var containerType = entityType.Model.FindEntityType(typeof(TContainer));
// Table splitting
containerType.SetTableName(entityType.GetTableName());
containerType.SetSchema(entityType.GetSchema());
// Shadow PK
var key = containerType.FindPrimaryKey() ?? containerType.SetPrimaryKey(entityType
.FindPrimaryKey().Properties
.Select(p => containerType.FindProperty(p.Name) ?? containerType.AddProperty(p.Name, p.ClrType))
.ToArray());
// Ownership
entityTypeBuilder
.HasOne(containerProperty)
.WithOne()
.HasForeignKey<TContainer>(key.Properties.Select(p => p.Name).ToArray());
// Relationship
return new ModelBuilder(entityType.Model)
.Entity<TContainer>()
.HasMany(collectionProperty);
}
}
}
Using the above custom method, the configuration of the sample model will be
modelBuilder.Entity<Blog>()
.HasMany(entity => entity.Posts, container => container.PostsCollection)
.WithOne()
.HasForeignKey(related => related.BlogId);
which is pretty much the same (just one additional lambda parameter) as the standard configuration if collection navigation property was directly on Blog
modelBuilder.Entity<Blog>()
.HasMany(entity => entity.PostsCollection)
.WithOne()
.HasForeignKey(related => related.BlogId);
It's not clear from the question, but I assume you only have the Blog and Post table in your database, and the Posts table does not exists and only has a class in the code.
You could have the Blog and Posts entities mapped to the same table as a splitted table and define the navigation property for that. For this you need to add one property to the Posts class (the Id as in the Blog) but you said you are only not allowed to change the Blog and Post classes, and if you need it to XML serialization, you can just mark this property with the [XmlIgnoreAttribute]
attribute.
public class Posts
{
[XmlIgnoreAttribute]
public int Id { get; set; }
public List<Post> PostsCollection { get; set; }
}
Then in your OnModelCreating
method:
modelBuilder.Entity<Blog>(entity => {
entity.ToTable("Blog");
entity.HasOne(b => b.Posts).WithOne().HasForeignKey<Blog>(b => b.Id);
});
modelBuilder.Entity<Posts>(entity => {
entity.ToTable("Blog");
entity.HasOne<Blog>().WithOne(b => b.Posts).HasForeignKey<Posts>(p => p.Id);
entity.HasMany(p => p.Post).WithOne().HasForeignKey(p => p.BlogId).HasPrincipalKey(p => p.Id);
});
modelBuilder.Entity<Post>(entity => {
entity.ToTable("Post");
entity.HasOne<Posts>().WithMany().HasForeignKey(p => p.BlogId).HasPrincipalKey(p => p.Id);
});