如何使用EF Core流畅API和值转换器创建外键



我在我的代码第一模型上使用强类型实体id,使用值转换器。

为了突出我遇到的问题,这里有一个简化的示例,该示例使用导航属性通过值转换器实现外键创建。

public class BlogId: EntityId
{
public BlogId(Guid value) : base(value)
{
}
}
public class Blog
{
public BlogId Id { get; set; }
public ICollection<Post> Posts { get; set; } // Explicitly define a relationship
}
public class PostId : EntityId
{
public PostId(Guid value) : base(value)
{
}
}
public class Post
{
public PostId Id { get; set; }
public BlogId BlogId { get; set; }
}

在数据上下文中设置

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
var blogIdConvertor = new ValueConverter<BlogId, Guid>(
u => u.Value, v => new BlogId(v));

var postIdConvertor = new ValueConverter<PostId, Guid>(
u => u.Value, v => new PostId(v));
modelBuilder.Entity<Blog>(u =>
{
u.HasKey(x => x.Id);
u.Property(x => x.Id).HasConversion(blogIdConvertor);
});

modelBuilder.Entity<Post>(u =>
{
u.HasKey(x => x.Id);
u.Property(x => x.Id).HasConversion(postIdConvertor);
u.Property(x => x.BlogId).HasConversion(blogIdConvertor);
});
}

在迁移中,外键被正确定义

migrationBuilder.CreateTable(
name: "post",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
blog_id = table.Column<Guid>(type: "uuid", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_post", x => x.id);
table.ForeignKey(
name: "fk_post_blogs_blog_temp_id",
column: x => x.blog_id,
principalTable: "blogs",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});

我需要定义一个外键关系,而不指定关系另一端的导航属性。

在上面的例子中,这意味着Blog模型看起来像这样。

public class Blog
{
public BlogId Id { get; set; }
// Remove the navigation
}

然后调整数据上下文以使用流体api来创建外键

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
var blogIdConvertor = new ValueConverter<BlogId, Guid>(
u => u.Value, v => new BlogId(v));

var postIdConvertor = new ValueConverter<PostId, Guid>(
u => u.Value, v => new PostId(v));
modelBuilder.Entity<Blog>(u =>
{
u.HasKey(x => x.Id);
u.Property(x => x.Id).HasConversion(blogIdConvertor);
});

modelBuilder.Entity<Post>(u =>
{
u.HasKey(x => x.Id);
u.Property(x => x.Id).HasConversion(postIdConvertor);
u.Property(x => x.BlogId).HasConversion(blogIdConvertor);
// Now use the fluent api to define the key, without the Blog side relation.
u.HasOne(x => x.BlogId).WithMany().HasForeignKey(x => x.BlogId);
});
}

运行迁移会出现以下错误:

The property or navigation 'BlogId' cannot be added to the entity 
type 'Post' because a property or navigation with the same name 
already exists on entity type 'Post'.

这是我努力寻找好的信息如何实现我所需要的。

我需要这个的原因是需要外键来支持数据库级别的数据完整性。我不需要它来在我的域模型中启用实际的遍历关系(我使用DDD聚合方法建模)

如何使用值转换器定义显式外键,使用流畅的api,而不必"污染";有不被期望的关系的模型?

答案来自EFCore GitHub,由ajcvickers回答。


上面代码中的基本问题是,它试图使用FK属性作为导航属性。相反,如果没有导航,那就把它去掉。例如:

u.HasOne<Blog>().WithMany().HasForeignKey(x => x.BlogId);

这里有一个完整的例子。确保你的值对象正确地实现相等。或者,考虑使用只读结构体。

public abstract class EntityId
{
protected EntityId(Guid value)
{
Value = value;
}
public Guid Value { get; }
private bool Equals(EntityId other)
=> Value.Equals(other.Value);
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj))
{
return false;
}
if (ReferenceEquals(this, obj))
{
return true;
}
if (obj.GetType() != GetType())
{
return false;
}
return Equals((EntityId)obj);
}
public override int GetHashCode()
=> Value.GetHashCode();
}
public class BlogId : EntityId
{
public BlogId(Guid value) : base(value)
{
}
}
public class Blog
{
public BlogId Id { get; set; }
}
public class PostId : EntityId
{
public PostId(Guid value) : base(value)
{
}
}
public class Post
{
public PostId Id { get; set; }
public BlogId BlogId { get; set; }
}
public class SomeDbContext : DbContext
{
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.UseSqlServer(Your.ConnectionString)
.LogTo(Console.WriteLine, LogLevel.Information)
.EnableSensitiveDataLogging();
public virtual DbSet<Blog> Blogs { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
var blogIdConvertor = new ValueConverter<BlogId, Guid>(
u => u.Value, v => new BlogId(v));
var postIdConvertor = new ValueConverter<PostId, Guid>(
u => u.Value, v => new PostId(v));
modelBuilder.Entity<Blog>(u =>
{
u.Property(x => x.Id).HasConversion(blogIdConvertor);
});
modelBuilder.Entity<Post>(u =>
{
u.Property(x => x.Id).HasConversion(postIdConvertor);
u.Property(x => x.BlogId).HasConversion(blogIdConvertor);
u.HasOne<Blog>().WithMany().HasForeignKey(x => x.BlogId);
});
}
}
public class Program
{
public static void Main()
{
using (var context = new SomeDbContext())
{
context.Database.EnsureDeleted();
context.Database.EnsureCreated();
var blog = new Blog {Id = new(Guid.NewGuid())};
var post = new Post {Id = new(Guid.NewGuid()), BlogId = blog.Id};
context.AddRange(blog, post);
context.SaveChanges();
}
using(var context = new SomeDbContext())
{
var post = context.Set<Post>().Single();
var blog = context.Set<Blog>().Find(post.BlogId);
Console.WriteLine(blog.Id.Value);
Console.WriteLine(post.Id.Value);
}
}
}

最新更新