CONTEXT_INFO 在执行 Entity Framework .SaveChanges 时丢失(过载)

CONTEXT_INFO lost while executing Entity Framework .SaveChanges (overload)

解法:

如您所见:here

  • The object context will open the connection if it is not already open before an operation. If the object context opens the connection during an operation, it will always close the connection when the operation is complete.
  • If you manually open the connection, the object context will not close it. Calling Close or Dispose will close the connection.

问题是 EF 会打开和关闭 SetUserContext 的连接,所以我会松开 CONTEXT_INFO。为了保留它,我需要手动打开连接并在 SaveChanges

之后关闭它
public int SaveChanges(string modifierId)
{
        Database.Connection.Open();
        SetUserContext(modifierId);
        var changes = base.SaveChanges();
        Database.Connection.Close();
        return changes;            
 }

问题:

系统在数据仓库上运行。数据库必须知道谁修改了它并保存审计中的任何更改 table.

为了达到这个结果,我主要依靠触发器和过程:

此函数将userId保存在CONTEXT_INFO中:

CREATE PROCEDURE [dbo].[SetUserContext]
    @userId NVARCHAR (64)
AS
BEGIN
    SET NOCOUNT ON;

    DECLARE @context VARBINARY(128)
    SET @context = CONVERT(VARBINARY(128), @userId)

    SET CONTEXT_INFO @context
END

而且这个可以在任何地方使用 获取 userId :

CREATE FUNCTION [dbo].[GetUserContext] ()
RETURNS NVARCHAR (64)
AS
BEGIN
    RETURN CONVERT(NVARCHAR (64), CONTEXT_INFO())
END

例如在我的触发器中我有:

CREATE TRIGGER UpdateUser 
ON [dbo].[Users] 
FOR UPDATE
  AS
    BEGIN
      INSERT INTO [Audit_Users]
      SELECT * , dbo.GetUserContext() , GETUTCDATE() , 0 FROM inserted
    END
GO

CREATE TABLE [dbo].[Users] (
    [Id]        NVARCHAR (64)  NOT NULL,
    [FirstName] NVARCHAR (255) NOT NULL,
    [LastName]  NVARCHAR (255) NOT NULL,
    [BirthDate] DATE           NOT NULL,
    [Type]      INT            NOT NULL,
    [Status]    INT            NOT NULL,
    [CreatorId] NVARCHAR (64)  NOT NULL,
    PRIMARY KEY CLUSTERED ([Id] ASC),
    CONSTRAINT [FK_Users_ToStatus] FOREIGN KEY ([Status]) REFERENCES [dbo].[StatusUsers] ([Id]),
    CONSTRAINT [FK_Users_ToCreator] FOREIGN KEY ([CreatorId]) REFERENCES [dbo].[Users] ([Id]),
    CONSTRAINT [FK_Users_ToType] FOREIGN KEY ([Type]) REFERENCES [dbo].[TypeUsers] ([Id])
);
CREATE TABLE [dbo].[Audit_Users] (
    [Id]         INT            IDENTITY (1, 1) NOT NULL,
    [UserId]     NVARCHAR (64)  NOT NULL,
    [FirstName]  NVARCHAR (255) NOT NULL,
    [LastName]   NVARCHAR (255) NOT NULL,
    [BirthDate]  DATE           NOT NULL,
    [Type]       INT            NOT NULL,
    [Status]     INT            NOT NULL,
    [CreatorId]  NVARCHAR (64)  NOT NULL,
    [ModifierId] NVARCHAR (64)  NOT NULL,
    [Date]       DATETIME       NOT NULL,
    [Deleted]    INT            NOT NULL,
    PRIMARY KEY CLUSTERED ([Id] ASC)
);

一切似乎都工作正常,当我用 sql 请求进行测试时,一切正常。 问题是我需要使用 Entity Framework 在我的 WCF 服务中调用它们。现在这就是麻烦开始的地方。 我 通过实体 设置 CONTEXT_INFO 并使用重载方法:

 public int SaveChanges(string modifierId)
    {
        SetUserContext(modifierId);
        return base.SaveChanges();
    }

但是当 base.SaveChanges();接到电话,我得到:

Cannot insert the value NULL into column 'ModifierId', table 'dbo.Audit_Users'; column does not allow nulls. INSERT fails. The statement has been terminated.

这表明我输了CONTEXT_INFO。我调试了(添加 table 并修改 setContext 过程并使用正确的值调用该过程)。

感谢您的帮助,我不是数据库专家,它可能很简单,但我被困在这里..

根据要求:

 public partial class Entities : DbContext
    {
        public Entities()
            : base("name=Entities")
        {
        }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            throw new UnintentionalCodeFirstException();
        }

        public virtual DbSet<Address> Addresses { get; set; }
        public virtual DbSet<Contact> Contacts { get; set; }
        public virtual DbSet<Email> Emails { get; set; }
        public virtual DbSet<File> Files { get; set; }
        public virtual DbSet<StatusUser> StatusUsers { get; set; }
        public virtual DbSet<TypeCommon> TypeCommons { get; set; }
        public virtual DbSet<TypeFile> TypeFiles { get; set; }
        public virtual DbSet<TypeUser> TypeUsers { get; set; }
        public virtual DbSet<User> Users { get; set; }
        public virtual DbSet<Workflow> Workflows { get; set; }

        public virtual int SetUserContext(string userId)
        {
            var userIdParameter = userId != null ?
                new ObjectParameter("userId", userId) :
                new ObjectParameter("userId", typeof(string));

            return ((IObjectContextAdapter)this).ObjectContext.ExecuteFunction("SetUserContext", userIdParameter);
        }
    }

创建用户:

public UserDto Create(string id, string firstName, string lastName, DateTime birthdate, string type,
    string modifierId)
{
    var userToReturn = new UserDto
    {
        Id = id,
        FirstName = firstName,
        LastName = lastName,
        Birthdate = birthdate,
        CreatorId = modifierId,
        Status = "Created",
        Type = type
    };
    using (var db = ContextFactory.GetEntities())
    {
        var user = Mapper.Map<User>(userToReturn);


        using (var transaction = new TransactionScope()) // this creates a new transaction
        {
            db.Users.Add(user);
            db.SetUserContext(modifierId);
            if (db.SaveChanges() == 1)
            {
                userToReturn = Mapper.Map<UserDto>(user);
                userToReturn.Type = type;
                userToReturn.Status = "Created";
                transaction.Complete();
            }
        }
    }
    return userToReturn;
}

您的 Context_Info() 为空的原因是因为存储过程在您调用它们后立即执行。当您执行 dataContext.SaveChanges() 时,它们不会被调用。您要做的是在与 dataContext.SaveChanges() 相同的事务中调用存储过程。为此,您的代码应该是这样的。

public partial class MyDbContext : DbContext 
{
    //...

    public virtual int SetUserContext(string modifierId)
    {
        return ((IObjectContextAdapter)this).ObjectContext.ExecuteFunction("SetUserContext", modifierId);  
    }
}

public class UserService 
{
    private MyDbContext m_dataContext;

    public UserService(MyDbContext dataContext)
    {
        m_dataContext = dataContext;
    {

    public User CreateUser(string firstName, string lastName, DateTime birthDate, int modifiedId) // list other parameters here
    {
        using (var transaction = new TransactionScope()) // this creates a new transaction
        {
            m_dataContext.Users.Add(new User()
            {
                //...
            });

            // instead of passing modified id into save changes, you can just call your stored procedure here
            m_dataContext.SetUserContext(modifiedId);

            // and then call the regular save changes
            m_dataContext.SaveChanges();

            transaction.Complete(); // this commits the transaction
        }
    }
}

注意 此处介绍的最终解决方案的体系结构目前不是很好。我建议实施存储库模式,而不是让服务访问数据上下文。

根据 documentation CONTEXT_INFO,

Returns the context_info value that was set for the current session or batch by using the SET CONTEXT_INFO statement.

"session or batch" 或多或少对应于 .NET 托管连接。这是对 EF connection management 有所帮助的地方。

默认的 EF 行为是相当自由地打开和关闭数据库连接 — 安全的是,.NET 连接池使这相当有效。在您的情况下,这意味着您的初始存储过程调用发生在与后续 EF 保存操作不同的 "session or batch" 中。

修复

这很容易解决:您只需要显式控制数据库连接即可。您可以通过为您的上下文对象提供一个构造函数重载来实现这一点,该重载提供到基础 DbContext class 的开放连接,或者通过在您的存储过程调用之前手动打开连接。

这样可以吗?

[Could you] suggest a prettier way to do it?

The whole point of using EntityFramework is to avoid having to manage SQL Connections. I feel something is wrong somewhere.

从底层实现中抽象出 EF 代码通常是不现实的。我不确定它是否特别可取。这种抽象通常在 repository/unit 工作层中更好。

EF 的 "whole point",恕我直言,是为了避免在数据库的原始数据和该数据的 .NET 对象表示之间进行大量样板代码转换。

(不过,有趣的是,EF 7 可能会更容易保持 ORM 抽象 "purer,",甚至提供适合在自动化测试中使用的内存提供程序。)