我可以使用无键实体类型来查询 Entity Framework Core 中的 CHANGETABLE 吗?

Can I use Keyless Entity Types to query CHANGETABLE in Entity Framework Core?

我正在使用 SQL 服务器更改跟踪,我正在尝试将 Microsoft Docs 中的这篇文章改编为 Entity Framework 应用程序:Work with Change Tracking.

我想 运行 这个 SQL 查询使用 Entity Framework:

SELECT
    P.*, CT.*
FROM
    dbo.Product AS P
RIGHT OUTER JOIN
    CHANGETABLE(CHANGES dbo.Product, @last_synchronization_version) AS CT
ON
    P.ProductID = CT.ProductID

这是我目前得到的:

public class Product
{
    public int ProductID { get; set; }

    // omitted dozens of other properties
}

public class ProductChange
{
    public int ProductID { get; set; }

    public Product? Product { get; set; }

    public long SYS_CHANGE_VERSION { get; set; }

    public long? SYS_CHANGE_CREATION_VERSION { get; set; }

    public char SYS_CHANGE_OPERATION { get; set; }

    public byte[]? SYS_CHANGE_COLUMNS { get; set; }

    public byte[]? SYS_CHANGE_CONTEXT { get; set; }
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<ProductChange>()
        .HasNoKey()
        .ToView(null);

    base.OnModelCreating(modelBuilder);
}
long lastSynchronizationVersion = ...; // obtained as described in "Work with Change Tracking"

const string sql = @"
    SELECT
        P.*, CT.*
    FROM
        dbo.Product AS P
    RIGHT OUTER JOIN
        CHANGETABLE(CHANGES dbo.Product, {0}) AS CT
    ON
        P.ProductID = CT.ProductID";

var changes = await dbContext.Set<ProductChange>.FromSqlRaw(sql, lastSynchronizationVersion);

它不起作用,因为 EF 不理解 P.* 如何映射到 public Product? Product { get; set; }。当我从查询中删除 Product 属性 并删除 P.* 时,一切都按预期进行。但是,我需要所有属性,而不仅仅是 ID。

Product 的所有属性复制到 ProductChange 并使它们全部可为空,但我真的不想诉诸于此。

在实践中,我不仅会为产品使用更改跟踪,还会为数十种实体类型使用更改跟踪,它们都有很多属性。必须在两个地方指定每个 属性 只是为了让 Entity Framework 很好地使用更改跟踪不是一个好主意。

有没有办法让无键实体类型做我想做的事?还是我必须使用 ADO.NET 的 ExecuteReader 并手动映射结果?

事实证明,您可以在无键实体类型上使用与导航属性的关系,就像您可以使用实体类型一样。

配置OnModelCreating中的关系:

modelBuilder.Entity<ProductChange>()
    .HasNoKey()
    .HasOne(x => x.Entity).WithMany().HasForeignKey(x => x.ProductID) // I added this line.
    .ToView(null);

现在您可以使用 Include 而不是手动连接表:

const string sql = "SELECT * FROM CHANGETABLE(CHANGES dbo.Product, {0}) AS CT";

var changes = await dbContext
    .Set<ProductChange>
    .FromSqlRaw(sql, lastSynchronizationVersion)
    .Include(x => x.Entity)
    .DefaultIfEmpty() // 
    .ToArrayAsync();

// it works!

此外(这是可选的),我创建了可以轻松继承的基本类型 ChangeChange<TEntity>

public abstract class Change
{
    public long Version { get; set; }
    public long? CreationVersion { get; set; }
    public char Operation { get; set; }
    public byte[]? Columns { get; set; }
    public byte[]? Context { get; set; }
}

public abstract class Change<TEntity> : Change
    where TEntity : class
{
    public TEntity? Entity { get; set; }
}
public ProductChange : Change<Product>
{
    public int ProductID { get; set; }
}

public OrderChange : Change<Order>
{
    public int OrderID { get; set; }
}

// etc...

您必须为 OnModelCreating 中的每个派生类型配置关系。

modelBuilder.Entity<ProductChange>()
    .HasOne(x => x.Entity).WithMany().HasForeignKey(x => x.ProductID);

modelBuilder.Entity<OrderChange>()
    .HasOne(x => x.Entity).WithMany().HasForeignKey(x => x.OrderID);

// etc...

你不必为每个实体重复 HasNoKey()ToView(null),而是添加这个循环:

foreach (var changeType in modelBuilder.Model.FindLeastDerivedEntityTypes(typeof(Change)))
{
    var builder = modelBuilder.Entity(changeType.ClrType).HasNoKey().ToView(null);

    builder.Property(nameof(Change.Version)).HasColumnName("SYS_CHANGE_VERSION");
    builder.Property(nameof(Change.CreationVersion)).HasColumnName("SYS_CHANGE_CREATION_VERSION");
    builder.Property(nameof(Change.Operation)).HasColumnName("SYS_CHANGE_OPERATION");
    builder.Property(nameof(Change.Columns)).HasColumnName("SYS_CHANGE_COLUMNS");
    builder.Property(nameof(Change.Context)).HasColumnName("SYS_CHANGE_CONTEXT");
}

如果需要,可以将 ID 属性 移动到 Change<TEntity>。通过这样做,您可以删除 ProductChangeOrderChange 等 类。但是,您必须指定列名,以便 Entity Framework Core 理解 ID 属性 从 Change<Product> 映射到 ProductID,而 ID 属性 从 Change<Order> 映射到 OrderID,等等。我选择不这样做,因为如果你有复合键,这种方法将不起作用。