使用 C# Entity Framework 在临时 Table 中插入记录

Insert Record in Temporal Table using C# Entity Framework

我在使用 C# Entity Framework

Temporal table 中插入数据时遇到问题

Table 架构是

CREATE TABLE People( 
    PeopleID int PRIMARY KEY NOT NULL, 
    Name varchar(50) Null, 
    LastName varchar(100) NULL, 
    NickName varchar(25), 
    StartTime datetime2 GENERATED ALWAYS AS ROW START NOT NULL, 
    EndTime datetime2 GENERATED ALWAYS AS ROW END NOT NULL, 
    PERIOD FOR SYSTEM_TIME (StartTime,EndTime) 
) WITH (SYSTEM_VERSIONING = ON(HISTORY_TABLE = dbo.PeopleHistory));

我照常创建了一个 EDMX,并尝试使用以下 C# 代码插入记录

using (var db = new DevDBEntities()) {
    People1 peo = new People1() {
        PeopleID = 1,
        Name = "Emma",
        LastName = "Watson",
        NickName = "ICE"
    };

    db.Peoples.Add(peo);
    db.SaveChanges();
}

我在 db.SaveChanges()

时遇到异常

"Cannot insert an explicit value into a GENERATED ALWAYS column in table 'DevDB.dbo.People'. Use INSERT with a column list to exclude the GENERATED ALWAYS column, or insert a DEFAULT into GENERATED ALWAYS column."

我尝试使用 SQL 服务器使用以下插入查询直接插入,它的插入很好。

INSERT INTO [dbo].[People]
           ([PeopleID]
           ,[Name]
           ,[LastName]
           ,[NickName])
     VALUES
           (2
           ,'John'
           ,'Math'
           ,'COOL')

请帮助我如何使用 C# 插入记录 Entity Framework。

简明摘要:当 EF 尝试更新 PERIOD 系统版本控制列中的值时出现问题,其中列 属性 值由 SQL 服务器本身。

MS Docs: Temporal tables 开始,时间 table 作为一对当前 table 和历史 table 解释如下:

System-versioning for a table is implemented as a pair of tables, a current table and a history table. Within each of these tables, the following two additional datetime2 columns are used to define the period of validity for each row:

Period start column: The system records the start time for the row in this column, typically denoted as the SysStartTime column.

Period end column: The system records the end time for the row in this column, typically denoted at the SysEndTime column.

由于 StartTimeEndTime 列都是自动生成的,因此必须排除对它们插入或更新值的任何尝试。假设您使用的是 EF 6,以下是消除错误的步骤:

  1. 在设计器模式下打开 EDMX 文件,在 StoreGeneratedPattern 选项中将 StartTimeEndTime 列属性设置为 Identity。这可以防止 EF 在任何 UPDATE 事件上刷​​新值。

  1. 创建自定义命令树拦截器class实现System.Data.Entity.Infrastructure.Interception.IDbCommandTreeInterceptor并指定设置子句应设置为ReadOnlyCollection<T> (T is a DbModificationClause),EF不能修改插入或更新修改:

    internal class TemporalTableCommandTreeInterceptor : IDbCommandTreeInterceptor
    {
        private static ReadOnlyCollection<DbModificationClause> GenerateSetClauses(IList<DbModificationClause> modificationClauses)
        {
            var props = new List<DbModificationClause>(modificationClauses);
            props = props.Where(_ => !_ignoredColumns.Contains((((_ as DbSetClause)?.Property as DbPropertyExpression)?.Property as EdmProperty)?.Name)).ToList();
    
            var newSetClauses = new ReadOnlyCollection<DbModificationClause>(props);
            return newSetClauses;
        }
    }
    
  2. 仍然在上面的 class 中,创建被忽略的 table 名称列表并在 INSERT 和 UPDATE 命令中定义操作,该方法应该如下所示(致谢此方法的 Matt Ruwe):

    // from /a/40742144
    private static readonly List<string> _ignoredColumns = new List<string> { "StartTime", "EndTime" };
    
    public void TreeCreated(DbCommandTreeInterceptionContext interceptionContext)
    {
        if (interceptionContext.OriginalResult.DataSpace == DataSpace.SSpace)
        {
            var insertCommand = interceptionContext.Result as DbInsertCommandTree;
            if (insertCommand != null)
            {
                var newSetClauses = GenerateSetClauses(insertCommand.SetClauses);
    
                var newCommand = new DbInsertCommandTree(
                    insertCommand.MetadataWorkspace,
                    insertCommand.DataSpace,
                    insertCommand.Target,
                    newSetClauses,
                    insertCommand.Returning);
    
                interceptionContext.Result = newCommand;
            }
    
            var updateCommand = interceptionContext.Result as DbUpdateCommandTree;
            if (updateCommand != null)
            {
                var newSetClauses = GenerateSetClauses(updateCommand.SetClauses);
    
                var newCommand = new DbUpdateCommandTree(
                updateCommand.MetadataWorkspace,
                updateCommand.DataSpace,
                updateCommand.Target,
                updateCommand.Predicate,
                newSetClauses,
                updateCommand.Returning);
    
                interceptionContext.Result = newCommand;
            }
        }
    }
    
  3. 在另一个代码部分使用数据库上下文之前,通过使用 DbInterception:

    注册上面的拦截器 class
    DbInterception.Add(new TemporalTableCommandTreeInterceptor());
    

    或使用 DbConfigurationTypeAttribute:

    将其附加到上下文定义中
    public class CustomDbConfiguration : DbConfiguration
    {
        public CustomDbConfiguration()
        {
            this.AddInterceptor(new TemporalTableCommandTreeInterceptor());
        }
    }
    
    // from /a/40302086
    [DbConfigurationType(typeof(CustomDbConfiguration))]
    public partial class DataContext : System.Data.Entity.DbContext
    {
        public DataContext(string nameOrConnectionString) : base(nameOrConnectionString)
        {
            // other stuff or leave this blank
        }
    }
    

相关问题:

Getting DbContext from implementation of IDbCommandInterceptor

可能最简单的解决方案是手动编辑 .EDMX 文件并删除 StartTime 和 EndTime 列的所有痕迹。