【问题标题】:Entity Framework Code First and SQL Server 2012 Sequences实体框架代码优先和 SQL Server 2012 序列
【发布时间】:2025-11-29 16:20:02
【问题描述】:

我正在实施数据库审计跟踪,通过我的 Web API 项目中的控制器执行的 CRUD 操作将序列化新旧 poco 并存储它们的值以供以后检索(历史、回滚等) .

当我完成所有工作时,我不喜欢它在 POST 期间让我的控制器看起来如何,因为我最终不得不调用 SaveChanges() 两次,一次是为了获取插入实体的 ID,然后再次提交需要知道该 ID 的审计记录。

我着手将项目(仍处于起步阶段)转换为使用序列而不是标识列。这有一个额外的好处,那就是进一步将我从 SQL Server 中抽象出来,虽然这不是一个真正的问题,但它也允许我减少提交的数量,并让我将逻辑从控制器中拉出并将其填充到我的服务层中从存储库中抽象出我的控制器,让我在这个“shim”层中像审计一样工作。

一旦创建了Sequence 对象和一个存储过程来公开它,我就创建了以下类:

public class SequentialIdProvider : ISequentialIdProvider
{
    private readonly IService<SequenceValue> _sequenceValueService;

    public SequentialIdProvider(IService<SequenceValue> sequenceValueService)
    {
        _sequenceValueService = sequenceValueService;
    }

    public int GetNextId()
    {
        var value = _sequenceValueService.SelectQuery("GetSequenceIds @numberOfIds", new SqlParameter("numberOfIds", SqlDbType.Int) { Value = 1 }).ToList();
        if (value.First() == null)
        {
            throw new Exception("Unable to retrieve the next id's from the sequence.");
        }

        return value.First().FirstValue;
    }

    public IList<int> GetNextIds(int numberOfIds)
    {
        var values = _sequenceValueService.SelectQuery("GetSequenceIds @numberOfIds", new SqlParameter("numberOfIds", SqlDbType.Int) { Value = numberOfIds }).ToList();
        if (values.First() == null)
        {
            throw new Exception("Unable to retrieve the next id's from the sequence.");
        }

        var list = new List<int>();
        for (var i = values.First().FirstValue; i <= values.First().LastValue; i++)
        {
            list.Add(i);
        }

        return list;
    }
}

它简单地提供了两种获取 ID 的方法,一个和一个范围。

在第一组单元测试中这一切都很好,但是当我开始在现实世界的场景中测试它时,我很快意识到对 GetNextId() 的一次调用将在该上下文的生命周期内返回相同的值, 直到 SaveChanges() 被调用,从而否定任何真正的好处。

我不确定是否有办法绕过创建第二个上下文(不是选项)或使用老式 ADO.NET 并进行直接 SQL 调用并使用 AutoMapper 来获得相同的最终结果。这些都不吸引我,所以我希望其他人有一个想法。

【问题讨论】:

  • 您的审计记录是什么样的?每张桌子一张,所有桌子一张?审计中存储了哪些信息?您审核哪些操作?你有通用数据层吗?
  • 包含以下数据的单个审计表:旧数据或新数据(或两者在 PUT 的情况下)的序列化 POCO、受影响的表、该表中的记录 ID、列表更改的列和生成审计的操作(插入、删除、更新)。
  • 那就没办法了。这两个 SaveChanges 选项看起来是唯一的解决方案。您不必为此担心两点,原因有两个:1) .NET 使用连接池,2) EF 不会批量发送查询,而是一个接一个地发送。因此,是否使用两个 SaveChanges 之间的差异应该可以忽略不计。唯一的其他选择是使用 GUID 作为 PK:*.com/questions/18200817/… 但这会增加更多缺点(索引性能差),因为 GUID 太长。
  • @JotaBe - 我并不太担心性能,而是我必须在哪里完成工作才能将它们联系在一起。我的 UoW 和存储库是相互独立的。我的存储库没有对我的 UoW 的引用,只有我的控制器有。因此,如果我使用双 SaveChanges() 方法,我必须在我的控制器中执行工作,这会增加一堆噪音,我宁愿将它们放在抽象存储库的服务类中。我使用我的容器制定了一个解决方案,将我的连接字符串注入到 ADONet 类中,并且暂时走这条路

标签: c# sql entity-framework sql-server-2012 sequence-sql


【解决方案1】:

不知道这是否对您有所帮助,但这是我首先使用代码进行审核日志跟踪的方式。 下面被编码成一个继承自 DbContext 的类。

在我的构造函数中,我有以下内容

IObjectContextAdapter objectContextAdapter = (this as IObjectContextAdapter);
objectContextAdapter.ObjectContext.SavingChanges += SavingChanges;

这是我之前设置的保存更改方法

void SavingChanges(object sender, EventArgs e) {
        Debug.Assert(sender != null, "Sender can't be null");
        Debug.Assert(sender is ObjectContext, "Sender not instance of ObjectContext");

        ObjectContext context = (sender as ObjectContext);
        IEnumerable<ObjectStateEntry> modifiedEntities = context.ObjectStateManager.GetObjectStateEntries(EntityState.Modified);
        IEnumerable<ObjectStateEntry> addedEntities = context.ObjectStateManager.GetObjectStateEntries(EntityState.Added);

        addedEntities.ToList().ForEach(a => {
            //Assign ids to objects that don't have
            if (a.Entity is IIdentity && (a.Entity as IIdentity).Id == Guid.Empty)
                (a.Entity as IIdentity).Id = Guid.NewGuid();

            this.Set<AuditLogEntry>().Add(AuditLogEntryFactory(a, _AddedEntry));
        });

        modifiedEntities.ToList().ForEach(m => {
            this.Set<AuditLogEntry>().Add(AuditLogEntryFactory(m, _ModifiedEntry));
        });
    }

这些是以前用来建立审计日志详细信息的方法

private AuditLogEntry AuditLogEntryFactory(ObjectStateEntry entry, string entryType) {
        AuditLogEntry auditLogEntry = new AuditLogEntry() {
            EntryDate = DateTime.Now,
            EntryType = entryType,
            Id = Guid.NewGuid(),
            NewValues = AuditLogEntryNewValues(entry),
            Table = entry.EntitySet.Name,
            UserId = _UserId
        };

        if (entryType == _ModifiedEntry) auditLogEntry.OriginalValues = AuditLogEntryOriginalValues(entry);

        return auditLogEntry;
    }

    /// <summary>
    /// Creates a string of all modified properties for an entity.
    /// </summary>
    private string AuditLogEntryOriginalValues(ObjectStateEntry entry) {
        StringBuilder stringBuilder = new StringBuilder();

        entry.GetModifiedProperties().ToList().ForEach(m => {
            stringBuilder.Append(String.Format("{0} = {1},", m, entry.OriginalValues[m]));
        });

        return stringBuilder.ToString();
    }

    /// <summary>
    /// Creates a string of all modified properties' new values for an entity.
    /// </summary>
    private string AuditLogEntryNewValues(ObjectStateEntry entry) {
        StringBuilder stringBuilder = new StringBuilder();

        for (int i = 0; i < entry.CurrentValues.FieldCount; i++) {
            stringBuilder.Append(String.Format("{0} = {1},",
                entry.CurrentValues.GetName(i), entry.CurrentValues.GetValue(i)));
        }

        return stringBuilder.ToString();
    }

希望这可以为您指明一个可能帮助您解决问题的方向。

【讨论】:

  • 我发现用这种方法(我把它全部连接起来并工作)是“CurrentValues”和“OriginalValues”在分离的、代码优先的存储库模式方法中是相同的。例如,在我的 RESTful Web api 中,当有更新时,我会查找正在更新的 ID(它是分离的),我进行必要的更改,然后将更新传递到存储库中。当 EF 附加实体时,原始值和当前值都相同。这是框架的一个已知限制(当时对我来说不是)。你可以在这里看到功能投票entityframework.codeplex.com/workitem/864
  • 感谢@JDBuckSavage 的评论,这是我不知道的事情,需要检查我的单元测试以确保我的应用程序正常运行。