c# - EF Core 导航属性未填充(再次!)

标签 c# ef-core-3.1

设置非常简单。端点和部署这两个表是多对多的,并由一个名为“服务”的表连接起来。

var query = from endpoint in db.Endpoints
    from deployment in endpoint.Service.Deployments
    select new Endpoint(
            deployment.Host,
            endpoint.Method,
            endpoint.Path,
            new Cost(endpoint.CostCredits, new PerRequest()));

var result = await query.ToListAsync();

当我运行这个时,我得到 0 个结果。

在调试器中检查 db.Endpoints[0].Service 显示 null。从稍微不同的角度来看:

var query = db.Endpoints
    .Include(endpoint => endpoint.Service)
    .ThenInclude(s => s.Deployments) // out of desperation!
    .SelectMany(endpoint => endpoint.Service.Deployments.Select(deployment =>
    new Endpoint(
        deployment.Host,
        endpoint.Method,
        endpoint.Path,
        new Cost(endpoint.CostCredits, new PerRequest()))));

这会引发 NullReferenceException。我什至尝试在查询之前添加此内容:

await db.Endpoints.ToListAsync();
await db.Deployments.ToListAsync();
await db.Services.ToListAsync();

相同的行为。

我见过很多这样的问题,例如 EF Core returns null relations until direct access但显然它们不是我的情况 - 即使加载所有三个表中的所有数据后,导航属性也不会填充。

如何让上述查询起作用?

这是一个完整的最小重现(与可运行的 Dotnetfiddle 相同):

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;

namespace Repro
{
    class Program
    {
        static async Task Main()
        {
            var db = new MorpherDbContext();
            db.AddSomeData();

            var query = from endpoint in db.Endpoints
                from deployment in endpoint.Service.Deployments
                select new {
                    deployment.Host,
                    endpoint.Method,
                    endpoint.Path
                    };

            var result = await query.ToListAsync();

            Console.WriteLine(result.Count);
        }
    }

    public class Deployment
    {
        public int Id { get; set; }
        public virtual Service Service { get; set; }
        public int ServiceId { get; set; }

        public string Host { get; set; }
        public short? Port { get; set; }
        public string BasePath { get; set; }
    }
    public class Service
    {
        public int Id { get; set; }
        public string Name { get; set; }

        public string UrlSlug { get; set; }

        public virtual ICollection<Endpoint> Endpoints { get; set; }

        public virtual ICollection<Deployment> Deployments { get; set; }
    }

    public class Endpoint
    {
        public int Id { get; set; }

        public virtual Service Service { get; set; }

        public int ServiceId { get; set; }
        public string Path { get; set; } 
        public string Method { get; set; } 
        public int CostCredits { get; set; } 
        public string CostType { get; set; } 
        public string CostTypeParameter { get; set; } 
    }

    public partial class MorpherDbContext : DbContext
    {
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseInMemoryDatabase("db1");
            //optionsBuilder.UseSqlServer("Server=(localdb)\\mssqllocaldb;Database=ReproDb;Trusted_Connection=True;MultipleActiveResultSets=true");
            base.OnConfiguring(optionsBuilder);
        }

        public virtual DbSet<Endpoint> Endpoints { get; set; }
        public virtual DbSet<Deployment> Deployments { get; set; }
        public virtual DbSet<Service> Services { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.HasAnnotation("Relational:DefaultSchema", "dbo");

            modelBuilder.Entity<Deployment>(entity =>
            {
                entity.Property(e => e.Host).IsRequired().HasMaxLength(256);
                entity.Property(e => e.BasePath).HasMaxLength(512);

                entity.HasOne(deployment => deployment.Service)
                    .WithMany(service => service.Deployments)
                    .HasForeignKey(d => d.ServiceId)
                    .OnDelete(DeleteBehavior.Restrict)
                    .HasConstraintName("FK_Deployments_Services");
            });

            modelBuilder.Entity<Service>(entity =>
            {
                entity.Property(e => e.Name).IsRequired().HasMaxLength(256);
                entity.Property(e => e.UrlSlug).IsRequired().HasMaxLength(256);
            });

            modelBuilder.Entity<Endpoint>(endpoint =>
            {
                endpoint.Property(e => e.Path).IsRequired();
                endpoint.Property(e => e.Method).IsRequired().HasMaxLength(6);
                endpoint.Property(e => e.CostCredits).IsRequired();
                endpoint.Property(e => e.CostType).IsRequired().HasMaxLength(50);
                endpoint.Property(e => e.CostTypeParameter).IsRequired().HasMaxLength(150);

                endpoint.HasOne(e => e.Service)
                    .WithMany(service => service.Endpoints)
                    .HasForeignKey(e => e.ServiceId)
                    .OnDelete(DeleteBehavior.Restrict)
                    .HasConstraintName("FK_Endpoints_Services");
            });

            OnModelCreatingPartial(modelBuilder);
        }

        partial void OnModelCreatingPartial(ModelBuilder modelBuilder);

        public void AddSomeData()
        {
            var ws3 = new Service { Name = "Веб-сервис «Морфер»", UrlSlug = "ws"};

            Services.Add(ws3);
            Deployments.Add(new Deployment {Service = ws3, Host = "ws3.morpher.ru"});

            Endpoints.AddRange(new []
            {
                new Endpoint {Method = "GET", Path = "russian/declension", CostCredits = 1, CostType = "PerRequest"},
                new Endpoint {Method = "POST", Path = "russian/declension", CostCredits = 1, CostType = "PerBodyLine"},
                new Endpoint {Method = "*", Path = "russian/userdict", CostCredits = 1, CostType = "PerRequest"},
            });

            ws3.Endpoints = Endpoints.ToList();

            SaveChanges();
        }
    }
}

namespace Repro.Migrations
{
    public partial class InitialCreate : Migration
    {
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.EnsureSchema(
                name: "dbo");

            migrationBuilder.CreateTable(
                name: "Services",
                schema: "dbo",
                columns: table => new
                {
                    Id = table.Column<int>(nullable: false)
                        .Annotation("SqlServer:Identity", "1, 1"),
                    Name = table.Column<string>(maxLength: 256, nullable: false),
                    UrlSlug = table.Column<string>(maxLength: 256, nullable: false)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_Services", x => x.Id);
                });

            migrationBuilder.CreateTable(
                name: "Deployments",
                schema: "dbo",
                columns: table => new
                {
                    Id = table.Column<int>(nullable: false)
                        .Annotation("SqlServer:Identity", "1, 1"),
                    ServiceId = table.Column<int>(nullable: false),
                    Host = table.Column<string>(maxLength: 256, nullable: false),
                    Port = table.Column<short>(nullable: true),
                    BasePath = table.Column<string>(maxLength: 512, nullable: true)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_Deployments", x => x.Id);
                    table.ForeignKey(
                        name: "FK_Deployments_Services",
                        column: x => x.ServiceId,
                        principalSchema: "dbo",
                        principalTable: "Services",
                        principalColumn: "Id",
                        onDelete: ReferentialAction.Restrict);
                });

            migrationBuilder.CreateTable(
                name: "Endpoints",
                schema: "dbo",
                columns: table => new
                {
                    Id = table.Column<int>(nullable: false)
                        .Annotation("SqlServer:Identity", "1, 1"),
                    ServiceId = table.Column<int>(nullable: false),
                    Path = table.Column<string>(nullable: false),
                    Method = table.Column<string>(maxLength: 6, nullable: false),
                    CostCredits = table.Column<int>(nullable: false),
                    CostType = table.Column<string>(maxLength: 50, nullable: false),
                    CostTypeParameter = table.Column<string>(maxLength: 150, nullable: false)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_Endpoints", x => x.Id);
                    table.ForeignKey(
                        name: "FK_Endpoints_Services",
                        column: x => x.ServiceId,
                        principalSchema: "dbo",
                        principalTable: "Services",
                        principalColumn: "Id",
                        onDelete: ReferentialAction.Restrict);
                });

            migrationBuilder.CreateIndex(
                name: "IX_Deployments_ServiceId",
                schema: "dbo",
                table: "Deployments",
                column: "ServiceId");

            migrationBuilder.CreateIndex(
                name: "IX_Endpoints_ServiceId",
                schema: "dbo",
                table: "Endpoints",
                column: "ServiceId");
        }

        protected override void Down(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.DropTable(
                name: "Deployments",
                schema: "dbo");

            migrationBuilder.DropTable(
                name: "Endpoints",
                schema: "dbo");

            migrationBuilder.DropTable(
                name: "Services",
                schema: "dbo");
        }
    }
}

namespace Repro.Migrations
{
    [DbContext(typeof(MorpherDbContext))]
    partial class MorpherDbContextModelSnapshot : ModelSnapshot
    {
        protected override void BuildModel(ModelBuilder modelBuilder)
        {
#pragma warning disable 612, 618
            modelBuilder
                .HasDefaultSchema("dbo")
                .HasAnnotation("ProductVersion", "3.1.3")
                .HasAnnotation("Relational:MaxIdentifierLength", 128)
                .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);

            modelBuilder.Entity("Repro.Deployment", b =>
                {
                    b.Property<int>("Id")
                        .ValueGeneratedOnAdd()
                        .HasColumnType("int")
                        .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);

                    b.Property<string>("BasePath")
                        .HasColumnType("nvarchar(512)")
                        .HasMaxLength(512);

                    b.Property<string>("Host")
                        .IsRequired()
                        .HasColumnType("nvarchar(256)")
                        .HasMaxLength(256);

                    b.Property<short?>("Port")
                        .HasColumnType("smallint");

                    b.Property<int>("ServiceId")
                        .HasColumnType("int");

                    b.HasKey("Id");

                    b.HasIndex("ServiceId");

                    b.ToTable("Deployments");
                });

            modelBuilder.Entity("Repro.Endpoint", b =>
                {
                    b.Property<int>("Id")
                        .ValueGeneratedOnAdd()
                        .HasColumnType("int")
                        .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);

                    b.Property<int>("CostCredits")
                        .HasColumnType("int");

                    b.Property<string>("CostType")
                        .IsRequired()
                        .HasColumnType("nvarchar(50)")
                        .HasMaxLength(50);

                    b.Property<string>("CostTypeParameter")
                        .IsRequired()
                        .HasColumnType("nvarchar(150)")
                        .HasMaxLength(150);

                    b.Property<string>("Method")
                        .IsRequired()
                        .HasColumnType("nvarchar(6)")
                        .HasMaxLength(6);

                    b.Property<string>("Path")
                        .IsRequired()
                        .HasColumnType("nvarchar(max)");

                    b.Property<int>("ServiceId")
                        .HasColumnType("int");

                    b.HasKey("Id");

                    b.HasIndex("ServiceId");

                    b.ToTable("Endpoints");
                });

            modelBuilder.Entity("Repro.Service", b =>
                {
                    b.Property<int>("Id")
                        .ValueGeneratedOnAdd()
                        .HasColumnType("int")
                        .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);

                    b.Property<string>("Name")
                        .IsRequired()
                        .HasColumnType("nvarchar(256)")
                        .HasMaxLength(256);

                    b.Property<string>("UrlSlug")
                        .IsRequired()
                        .HasColumnType("nvarchar(256)")
                        .HasMaxLength(256);

                    b.HasKey("Id");

                    b.ToTable("Services");
                });

            modelBuilder.Entity("Repro.Deployment", b =>
                {
                    b.HasOne("Repro.Service", "Service")
                        .WithMany("Deployments")
                        .HasForeignKey("ServiceId")
                        .HasConstraintName("FK_Deployments_Services")
                        .OnDelete(DeleteBehavior.Restrict)
                        .IsRequired();
                });

            modelBuilder.Entity("Repro.Endpoint", b =>
                {
                    b.HasOne("Repro.Service", "Service")
                        .WithMany("Endpoints")
                        .HasForeignKey("ServiceId")
                        .HasConstraintName("FK_Endpoints_Services")
                        .OnDelete(DeleteBehavior.Restrict)
                        .IsRequired();
                });
#pragma warning restore 612, 618
        }
    }
}

最佳答案

问题在于以下行:

ws3.Endpoints = Endpoints.ToList();

这一行会弄乱 Entity Framework 当前上下文维护的导航属性/集合。事实上,当您尝试在 AddSomeData() 方法中为 Endpoint 对象设置 Service = ws3 时,您将收到以下异常消息:

The association between entities 'Service' and 'Endpoint' with the key value '{ServiceId: 1}' has been severed but the relationship is either marked as 'Required' or is implicitly required because the foreign key is not nullable. If the dependent/child entity should be deleted when a required relationship is severed, then setup the relationship to use cascade deletes.

您正在通过 Add()/AddRange() 方法添加 Endpoint 对象,但没有分配任何 Service 引用(或 ServiceId 值集)。与上面的行结合起来它会中断(以某种方式)。

要解决此问题,您应该删除 ws3.Endpoints = Endpoints.ToList() 行并设置 Service 导航属性,或者删除 Add( )/AddRange() 调用 Endpoint 对象。 AddSomeData() 方法可能如下所示:

public void AddSomeData()
{
    var ws3 = new Service { Name = "Веб-сервис «Морфер»", UrlSlug = "ws"};

    Services.Add(ws3);
    Deployments.Add(new Deployment {Service = ws3, Host = "ws3.morpher.ru"});
    var endpointsToAdd = new []
    {
        new Endpoint {Method = "GET", Path = "russian/declension", CostCredits = 1, CostType = "PerRequest"},
        new Endpoint {Method = "POST", Path = "russian/declension", CostCredits = 1, CostType = "PerBodyLine"},
        new Endpoint {Method = "*", Path = "russian/userdict", CostCredits = 1, CostType = "PerRequest"},
    };

    ws3.Endpoints = endpointsToAdd.ToList();

    SaveChanges();
}

另一个解决方案是:

public void AddSomeData()
{
    var ws3 = new Service { Name = "Веб-сервис «Морфер»", UrlSlug = "ws"};

    Services.Add(ws3);
    Deployments.Add(new Deployment {Service = ws3, Host = "ws3.morpher.ru"});
    Endpoints.Add(new Endpoint {Service = ws3, Method = "GET", Path = "russian/declension", CostCredits = 1, CostType = "PerRequest"});
    Endpoints.Add(new Endpoint {Service = ws3, Method = "POST", Path = "russian/declension", CostCredits = 1, CostType = "PerBodyLine"});
    Endpoints.Add(new Endpoint {Service = ws3, Method = "*", Path = "russian/userdict", CostCredits = 1, CostType = "PerRequest"});

    SaveChanges();
}

关于c# - EF Core 导航属性未填充(再次!),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/62377299/

相关文章:

c# - 字符串中的 URL 被撇号损坏

c# - 针对 EF Core 和 SQL Server 数据库编写集成测试

entity-framework-core - EF核心: Lazy-loaded navigations must have backing fields

c# - 我加入 .NetCore 3.1 引发有关 NavigationExpandingExpressionVisitor 的异常,那是什么?

c# - 从数据库中检索数据时 DBNull 为其他类型

C# 获取ssl服务器证书的绑定(bind)

c# - 管理同一服务的多个 WCF 端点

c# - RavenDB 错误 : I can't access var store from a different class. 任何建议

c# - 我想要一种基于 Entity Framework 中的 where 子句更新一系列记录的方法,而不使用 ToList() 和 foreach

docker - 无法使用 EF Core Mysql 提供程序将 Docker 中的 Asp.net Core Web Api 连接到主机服务器上的非 Docker Mysql