Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to define navigation for in-class encapsulated property?

I inherited a shared project, where models are defined. For easier XML serialization they are in the form:

    public class Blog
    {
        public int Id { get; set; }
        public Posts Posts { get; set; }
    }

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

    public class Post
    {
        public int BlogId { get; set; }
        public int PostId { get; set; }
        public string Content { get; set; }
    }

How do I specify EF DbContext in OnModelCreating method to use Posts.PostsCollection as navigation property? Let's assume, I am not allowed to change anything in Post and Blog classes. I just need to programmatically specify relations for EF. Is it possible? I have read about defining relationships on MS site and also other topics about defining model on this site and various others, but couldn't find anything for my scenario.

like image 923
Janez Lukan Avatar asked Jan 20 '21 10:01

Janez Lukan


2 Answers

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);
like image 98
Ivan Stoev Avatar answered Oct 17 '22 22:10

Ivan Stoev


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);
});
like image 44
Annosz Avatar answered Oct 17 '22 22:10

Annosz