5

Solution:

As you can find : 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.

The problem was that the EF would open and close the connection for SetUserContext, so I would loose the CONTEXT_INFO. In order to keep it I need to open connection manually and close it after the SaveChanges

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

Question :

The system work on a datawarehouse. The database has to know who modifies it and saves any change in an Audit table.

To achieve this result I rely mostly on triggers and and procedures :

This function Saves the userId in 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

And this one can be use anywhere to Get the userId :

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

For instance in my trigger I have:

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)
);

Everything seemed to work fine, when I test in with sql request and all it all works. Problem is that I need to call them in my WCF service using Entity Framework. And now that's where trouble begins. I set CONTEXT_INFO through entity with an overload method:

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

But when the base.SaveChanges(); Gets called, I get:

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

Which suggest that I lost CONTEXT_INFO. I debugged (Adding a table and modifying the setContext procedure and the procedure is called with the proper values).

Thanks for your help I'm no database expert it might be something pretty simple but I'm stuck here..

As requested:

 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);
        }
    }

Create User:

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;
}
Lomithrani
  • 2,033
  • 3
  • 18
  • 24
  • Instead of "INSERT INTO [Audit_Users] Select *, dbo.GetUserContext(), ..", can you try breaking down your trigger into a few different statements? First call dbo.GetUserContext() and save the result of that stored procedure into a variable. Then list out all the column names in both insert and the select statements. Does it work if you write it that way? – Parth Shah Jul 15 '15 at 10:05
  • @ParthShah no it doesn't work either. With : (arobat signs deleted to be able to comment) CREATE TRIGGER InsertUser ON [dbo].[Users] FOR INSERT AS BEGIN DECLARE ModifierId NVARCHAR(64); SET ModifierId = dbo.GetUserContext(); INSERT INTO [Audit_Users](UserId,FirstName,LastName,BirthDate,Type,Status,CreatorId,ModifierId,Date,Deleted) SELECT Id, FirstName, LastName, BirthDate, Type, Status ,CreatorId , ModifierId , GETUTCDATE(), 0 FROM inserted END – Lomithrani Jul 15 '15 at 10:23
  • See also http://davecallan.com/passing-userid-delete-trigger-entity-framework/# and http://stackoverflow.com/questions/295710/logging-every-data-change-with-entity-framework – Rory Nov 08 '16 at 15:13

2 Answers2

2

According to the documentation CONTEXT_INFO,

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

The "session or batch" more or less corresponds to the .NET managed connection. This is where understanding a bit about EF connection management helps somewhat.

The default EF behavior is to open and close the database connection quite freely—safe in the knowledge that .NET connection pooling makes this reasonably efficient. In your case, this will mean that your initial stored procedure call happens in a different "session or batch" to the subsequent EF save operation.

The Fix

This is quite easy to fix: you just need to take explicit control over the database connection. You can do this either by providing a constructor overload for your context object that supplies an open connection to the base DbContext class, or by opening the connection manually before your stored procedure call.

Is This OK?

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

and

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

It's usually not realistic to abstract EF code away from the underlying implementation. I'm not sure it's even particularly desirable. This abstraction is usually better with a repository/unit of work layer.

The "whole point" of EF, IMHO, is to avoid lots of boiler-plate code translating between raw data from the database and .NET object representations of that data.

(Interestingly, though, EF 7 will probably make it easier to keep the ORM abstraction "purer," even providing an in-memory provider suitable for use in automated tests.)

Olly
  • 5,966
  • 31
  • 60
1

The reason why your Context_Info() is null is because stored procedures are executed immediately after you invoke them. They are not invoked when you do dataContext.SaveChanges(). What you want to do is invoke the stored procedure within the same transaction as the dataContext.SaveChanges(). In order to do so, this is how your code should really look like.

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
        }
    }
}

NOTE The architecture of the end solution as presented here is not that good at the moment. I would recommend implementing the Repository pattern instead of letting the Service have access to the data context.

Parth Shah
  • 2,060
  • 1
  • 23
  • 34
  • Tried with EXEC @ModifierId = db.GetuserContect; (because () gives incorrect syntax). And I don't have any better result. I must insist all my test in sql with my triggers works. I get the context and all if I execute sql. I think the problem comes from entity framework. (Maybe it recreates a connection thus erase the Context_Info) – Lomithrani Jul 15 '15 at 10:32
  • My bad. I didn't realize GetUserContext was a function and not stored procedure. – Parth Shah Jul 15 '15 at 10:35
  • Should I make it a stored procedure? – Lomithrani Jul 15 '15 at 11:15
  • I don't think it should matter. Please look at my updated answer and let me know if insertion happens successfully. – Parth Shah Jul 15 '15 at 11:15
  • @Lomithrani can you share implementation of your SetUserContext(). I want to know whether you use the same dbContext or not. Meanwhile I will share how the code should look like. – Parth Shah Jul 15 '15 at 11:33
  • I edited the question to include setUserContext, it is auto generated by entity framework from the database – Lomithrani Jul 15 '15 at 11:38
  • I adapted your method to my User Create (see edited question) Unfortunatly I still get the same error :( – Lomithrani Jul 15 '15 at 12:01
  • 1
    You may need to open the connection manually, before the call to `SetUserContext()`. I think EF automatically opens and closes connections on each operation unless you explicitly take control of doing it. – Olly Jul 15 '15 at 12:25
  • @Olly I have never had this issue. – Parth Shah Jul 15 '15 at 12:28
  • @Lomithrani you are 100% sure that your modifierId is not empty. Can you try running SQL Profiler and executing each statement one by one? – Parth Shah Jul 15 '15 at 12:28
  • Yeah 100%. I debugged in visual studio , and even created a table and altered setContextInfo to write the value of the userId in another table and setcontextinfo was triggered and the correct value was inserted in the table. SetContextInfo in correctly called by my app, I'm 99,9% sure that EF loose the value (most logical explanation is that it changes the connection as @Olly suggested). I'm quite new to all this (started my first internship / c# project, 2 months ago). I'll look more into Olly suggestion. – Lomithrani Jul 15 '15 at 13:01
  • 1
    @Olly seems like you were right, If you want to post your comment as an answer for me to accept it as the answer feel free to do it, otherwise I'll just Post mine. And by the way if you could suggest a prettier way to do it, as I don't have access to the context inside the SaveChanges overload , I have to open the connection before the saveChanges call and close it after (which is not really pretty) – Lomithrani Jul 15 '15 at 13:22
  • @Lomithrani I know things are working now for you but can you post your code for ContextFactory.GetEntities()? The whole point of using EntityFramework is to avoid having to manage SQL Connections. I feel something is wrong somewhere. – Parth Shah Jul 15 '15 at 13:31
  • @ParthShah Sure, just give me 5 minutes. As I read somewhere Olly suggestion makes sense. EF does Open and close connection for each operation. Calling the procedure is an operation and saving changes another, so I lost CONTEXT_INFO in between. See this link https://msdn.microsoft.com/en-us/library/bb738582(v=vs.90).aspx – Lomithrani Jul 15 '15 at 13:34
  • #if DEBUG public static TransactionScope Scope { get; set; } #endif //#if DEBUG // Scope = new TransactionScope(); // //var test = Substitute.For(); // // test.Users.Returns() // // return test; //#endif public static Entities GetEntities() { return new Entities(); } When I see this your answer might be right also, because I'm in debug I'm in a transaction scope maybe it doesn't like nested transaction scope – Lomithrani Jul 15 '15 at 13:35
  • NVM, I never use the transaction scope even in debug, only for my tests so it shouldn't have any incidence – Lomithrani Jul 15 '15 at 13:37
  • @ParthShah I think that might be one of the rare case where you actually need to handle connection manually. – Lomithrani Jul 15 '15 at 14:38