SaveChangesAsync只工作一次,然后总是失败

时间:2018-02-18 12:11:17

标签: c# entity-framework-core asp.net-core-webapi

我的一个查询遇到了一个奇怪的问题。当我第一次执行它时它可以工作,但所有后续调用都失败了

  

无法在表格'消息'。

中插入标识列的显式值

当我重新启动网络服务时,它也会再次运行。

我的表的架构是:

internal class Message
{
    public long Id { get; set; }
    public int TimeRangeId { get; set; }
    public byte[] Body { get; set; }
    public string BodyHash { get; set; }
    public DateTime? DeletedOn { get; set; }
    #region Navigation properties
    public TimeRange TimeRange { get; set; }
    #endregion
}

internal class Queue
{
    public int Id { get; set; }
    public string Name { get; set; }
    #region Navigation properties
    public TimeRange TimeRange { get; set; }
    #endregion
}

internal class TimeRange
{
    public int Id { get; set; }
    public int QueueId { get; set; }
    public DateTime StartsOn { get; set; }
    public DateTime EndsOn { get; set; }
    public DateTime CreatedOn { get; set; }
    #region Navigation properties
    public Queue Queue { get; set; }
    public List<Message> Messages { get; set; }
    #endregion
}

其中上下文设置如下:

internal class MessageQueueContext : DbContext
{
    private readonly string _connectionString;

    private readonly string _schema;

    public MessageQueueContext(string connectionString, string schema)
    {
        _connectionString = connectionString;
        _schema = schema;
    }

    public DbSet<TimeRange> TimeRanges { get; set; }

    public DbSet<Message> Messages { get; set; }

    public DbSet<Queue> Queues { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder
            .UseSqlServer(_connectionString);
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {            
        modelBuilder.Entity<Message>(e =>
        {
            e.ToTable(nameof(Message), _schema).HasKey(t => t.Id);
            e.Property(p => p.Id).UseSqlServerIdentityColumn();
            e.Property(p => p.TimeRangeId).IsRequired();
            e.Property(p => p.Body).IsRequired();
            e.Property(p => p.BodyHash).IsRequired();
        });

        modelBuilder.Entity<TimeRange>(e =>
        {
            e.ToTable(nameof(TimeRange), _schema).HasKey(t => t.Id);
            e.Property(p => p.Id).UseSqlServerIdentityColumn();
            e.Property(p => p.StartsOn).IsRequired();
            e.Property(p => p.EndsOn).IsRequired();
            e.Property(p => p.CreatedOn).ValueGeneratedOnAdd();
        });

        modelBuilder.Entity<Queue>(e =>
        {
            e.ToTable(nameof(Queue), _schema).HasKey(t => t.Id);
            e.Property(p => p.Id).UseSqlServerIdentityColumn();
            e.Property(p => p.Name).IsRequired();
        });
    }
}

失败的查询就是这个:

public async Task<int> EnqueueAsync(DateTime timeRangeStartsOn, DateTime timeRangeEndsOn, IEnumerable<byte[]> bodies, CancellationToken cancellationToken)
{
    const int nothingEnqueued = 0;

    using (var context = new MessageQueueContext(_connectionString, _schema))
    {
        context.Queues.Add(_queue.Value);
        context.Entry(_queue.Value).State = EntityState.Unchanged;

        var messages =
            (from body in bodies
             let bodyHash = _computeBodyHash(body).ToHexString()
             where true // CanEnqueueDuplicates || !context.Messages.AsNoTracking().Any(m => m.BodyHash == bodyHash && m.DeletedOn == null)
             select new Message
             {
                 Body = body,
                 BodyHash = bodyHash
             }).ToList();

        if (!CanEnqueueEmptyTimeRange && !messages.Any())
        {
            return nothingEnqueued;
        }

        var timeRange = new Entities.TimeRange
        {
            Queue = _queue.Value,
            StartsOn = timeRangeStartsOn,
            EndsOn = timeRangeEndsOn,
            Messages = messages
        };

        await context.TimeRanges.AddAsync(timeRange, cancellationToken);
        return await context.SaveChangesAsync(cancellationToken);
    }
}

它正确生成Message.Id,并且它也在 Identity 中正确设置在数据库中。

我使用 SQL Server Profiler 调试它,发现第二个请求与第一个请求不同:

这是第一个电话产生的内容:

exec sp_executesql N'SET NOCOUNT ON;
INSERT INTO [smq].[TimeRange] ([EndsOn], [QueueId], [StartsOn])
VALUES (@p0, @p1, @p2);
SELECT [Id], [CreatedOn]
FROM [smq].[TimeRange]
WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();

',N'@p0 datetime2(7),@p1 int,@p2 datetime2(7)',@p0='2018-05-02 00:00:00',@p1=1,@p2='2018-05-01 00:00:00'


exec sp_executesql N'SET NOCOUNT ON;
DECLARE @inserted0 TABLE ([Id] bigint, [_Position] [int]);
MERGE [smq].[Message] USING (
VALUES (@p3, @p4, @p5, @p6, 0),
(@p7, @p8, @p9, @p10, 1)) AS i ([Body], [BodyHash], [DeletedOn], [TimeRangeId], _Position) ON 1=0
WHEN NOT MATCHED THEN
INSERT ([Body], [BodyHash], [DeletedOn], [TimeRangeId])
VALUES (i.[Body], i.[BodyHash], i.[DeletedOn], i.[TimeRangeId])
OUTPUT INSERTED.[Id], i._Position
INTO @inserted0;

SELECT [t].[Id] FROM [smq].[Message] t
INNER JOIN @inserted0 i ON ([t].[Id] = [i].[Id])
ORDER BY [i].[_Position];

',N'@p3 varbinary(8000),@p4 varbinary(8000),@p5 datetime2(7),@p6 int,@p7 varbinary(8000),@p8 varbinary(8000),@p9 datetime2(7),@p10 int',@p3=0x7,@p4=0x7,@p5=NULL,@p6=21,@p7=0x7,@p8=0x1,@p9=NULL,@p10=21

这就是EF在所有后续调用中生成的内容:

exec sp_executesql N'SET NOCOUNT ON;
INSERT INTO [smq].[Message] ([Id], [Body], [BodyHash], [DeletedOn], [TimeRangeId])
VALUES (@p0, @p1, @p2, @p3, @p4),
(@p5, @p6, @p7, @p8, @p9);
INSERT INTO [smq].[TimeRange] ([EndsOn], [QueueId], [StartsOn])
VALUES (@p10, @p11, @p12);
SELECT [Id], [CreatedOn]
FROM [smq].[TimeRange]
WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();

',N'@p0 bigint,@p1 varbinary(8000),@p2 varbinary(8000),@p3 datetime2(7),@p4 int,@p5 bigint,@p6 varbinary(8000),@p7 varbinary(8000),@p8 datetime2(7),@p9 int,@p10 datetime2(7),@p11 int,@p12 datetime2(7)',@p0=21,@p1=0x7,@p2=0x7,@p3=NULL,@p4=21,@p5=22,@p6=0x7,@p7=0x1,@p8=NULL,@p9=21,@p10='2018-05-02 00:00:00',@p11=1,@p12='2018-05-01 00:00:00'

我错过了明显的吗?呼叫不同看起来并不正常。他们不应该,我是对的吗?

如果您需要任何其他信息,请通知我,我会修改此问题。

1 个答案:

答案 0 :(得分:1)

我发现了这个错误。

private readonly Lazy<Entities.Queue> _queue;

public async Task<int> EnqueueAsync(DateTime timeRangeStartsOn, DateTime timeRangeEndsOn, IEnumerable<byte[]> bodies, CancellationToken cancellationToken)
{
    const int nothingEnqueued = 0;

    using (var context = new MessageQueueContext(_connectionString, _schema))
    {
        context.Queues.Add(_queue.Value);
        context.Entry(_queue.Value).State = EntityState.Unchanged;

最后两行我将有关当前队列的信息添加到上下文中。由于这是一个字段,它似乎在上下文之间存活,并以某种方式使其受损。

我将此更改为

context.Queues.Attach(new Entities.Queue 
{
    Id = _queue.Value.Id, 
    Name = _queue.Value.Name 
});

现在它没有问题。简单地附加不会修复错误(它比添加和更改状态更容易)。

真正的修复是创建实体的副本,因为db-context不喜欢它的某些内容。我想人们需要知道EF的内部才能理解它......