用于编辑插入的EF 6.1 IDbCommandTreeInterceptor不会更新生成的实体属性

时间:2015-11-16 13:06:15

标签: .net sql-server entity-framework code-first

根据我发现here的文章,我实现了一个多租户IDbCommandInterceptor。 我注意到实现不会更新我作为IDbCommandInterceptor的一部分插入的模型上的值。我希望解决这个问题。

我使用拦截器为基本模型上的以下属性添加正确的值:

  • EntityTenantId
  • 创建
  • CreatedBy

此外,我更改DbInsertCommandTree以使用包含那些字段的DbNewInstanceExpression,除了EntityId(Identity)&时间戳。

尽管我添加的3个额外字段正确查询,但只有数据库生成的字段(EntityId& Timestamp)在模型中更新。

问题: 对于通常不属于数据库生成类别的字段,有没有人知道如何在插入后让EF更新我的模型? 换句话说,在调用SaveChanges()以保存新实体之后,该实体将具有EntityId&的更新属性值。时间戳。如何确保在模型实例上更新3个额外属性?

单元测试:

using (new MockRuntimeContext(ConstantPrincipals.DefaultTestUser))
            {
                using (var dbContext = new EndUserDbContext(TestConstants.EndUserDatabaseName))
                {
                    var newPerson = dbContext.Persons.Create();
                    newPerson.FirstName = "InsertEntityFname";
                    newPerson.LastName = "InsertEntityLname";
                    newPerson.DateOfBirth = DateTime.Now.Subtract(TimeSpan.FromDays(365*29));

                    dbContext.Persons.Add(newPerson);
                    dbContext.SaveChanges();

                    Assert.AreNotEqual(newPerson.EntityId, Guid.Empty);
                    //Fails.. but it's value in the database is correct.
                    Assert.AreEqual(newPerson.EntityTenantId, RuntimeContext.GetCurrentTenantIdForDataInsertion());
                    //Fails.. but it's value in the database is correct.
                    Assert.AreEqual(newPerson.EntityCreatedBy, RuntimeContext.GetAuthenticatedUserId());
                    //Fails.. but it's value in the database is correct.
                    Assert.AreNotEqual(newPerson.EntityCreated, null);
                }
            }

基础模型:

public class BaseEntity : ITenantAwareEntity, ISoftDeleteEntity, ITimestampEntity
{
    [DatabaseGenerated(DatabaseGeneratedOption.Identity), Key]
    public  Guid EntityId { get; set; }
    public virtual DateTime EntityCreated { get; private set; }


    public virtual Guid EntityCreatedBy { get; private set; }

    public virtual DateTime? EntityUpdated { get; private set; }

    public virtual Guid? EntityUpdatedBy { get; private set; }

    /// <summary>
    /// if the EntityTenantId is set the entity is scoped towards the tenant corresponding to the
    /// tenant with that ID.
    /// If the EntityTenantId is not set, the entity is available to everybody.
    /// </summary>
    public virtual Guid? EntityTenantId { get; private set; }

    public virtual bool EntityIsDeleted { get; set; }

    /// <summary>
    /// An entity timestamp for row version concurrency checks.
    /// </summary>
    [Timestamp]
    public virtual byte[] EntityTimeStamp { get; private set; }

结果插入查询:

"DECLARE @generated_keys table([EntityId] uniqueidentifier)
INSERT [Actor].[Persons]([FirstName], [LastName], [DateOfBirth_ValueLong], [PlaceofBirth], [Comment], [ExternalReference], [EntityUpdated], [EntityUpdatedBy], [EntityIsDeleted], [CountryOfOrigin_EntityId], [Language_EntityId], [MaritalStatus_EntityId], [Nationality_EntityId], [OccupationType_EntityId], [Sex_EntityId], [EntityTenantId], [EntityCreatedBy], [EntityCreated])\r\nOUTPUT inserted.[EntityId] INTO @generated_keys\r\nVALUES (@0, @1, @2, NULL, NULL, NULL, NULL, NULL, @3, NULL, NULL, NULL, NULL, NULL, NULL, @4, @5, @6)
SELECT t.[EntityId], t.[EntityTimeStamp], t.[EntityTenantId], t.[EntityCreatedBy], t.[EntityCreated]
FROM @generated_keys AS g JOIN [Actor].[Persons] AS t ON g.[EntityId] = t.[EntityId]
WHERE @@ROWCOUNT > 0"

这是我使用的拦截器(它被调用,并且所有属性都设置为拦截器指定的值。

public class TenantCommandTreeInterceptor : IDbCommandTreeInterceptor
    {
        private readonly MultiTenantAccessFacilitator _multiTenantAccessFacilitator;

        public TenantCommandTreeInterceptor(MultiTenantAccessFacilitator multiTenantAccessFacilitator)
        {
            _multiTenantAccessFacilitator = multiTenantAccessFacilitator;
        }

        public void TreeCreated(DbCommandTreeInterceptionContext interceptionContext)
        {
            if (interceptionContext.OriginalResult.DataSpace != DataSpace.SSpace) return;

            // Check that there is an authenticated user in this context
            var identity = Thread.CurrentPrincipal.Identity as ClaimsIdentity;
            if (identity == null || identity.IsAuthenticated == false)
            {
                return;
            }
            var userIdclaim = identity.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier);
            if (userIdclaim == null)
            {
                return;
            }

            var currentUserIdExctractedFromClaimsPrincipal = Guid.Parse(userIdclaim.Value);

            if (interceptionContext.Result.CommandTreeKind == DbCommandTreeKind.Insert)
            {
                InterceptInsertStatement(interceptionContext, currentUserIdExctractedFromClaimsPrincipal);
                return;
            }
            else if (interceptionContext.Result.CommandTreeKind == DbCommandTreeKind.Update)
            {

                InterceptUpdateCommand(interceptionContext, currentUserIdExctractedFromClaimsPrincipal);
                return;
            }
            else if(interceptionContext.Result.CommandTreeKind == DbCommandTreeKind.Query)
            {
                var queryCommand = interceptionContext.Result as DbQueryCommandTree;
                if (queryCommand != null)
                {
                    var newQuery =
                        queryCommand.Query.Accept(
                            new TenantSelectionFilterQueryVisitor(_multiTenantAccessFacilitator));
                    interceptionContext.Result = new DbQueryCommandTree(
                        queryCommand.MetadataWorkspace,
                        queryCommand.DataSpace,
                        newQuery);
                    return;

                }
            }
        }

        private void InterceptUpdateCommand(DbCommandTreeInterceptionContext interceptionContext,
            Guid currentUserIdExctractedFromClaimsPrincipal)
        {
            var updateCommand = interceptionContext.Result as DbUpdateCommandTree;

            List<DbSetClause> replacedSetClause = new List<DbSetClause>();
            List<DbSetClause> autoSetClause = new List<DbSetClause>();

            //UpdatedBy
            var column = nameof(BaseEntity.EntityUpdatedBy);
            DbSetClause existingSetClause, newClause;
            DbExpression newValue =
                DbExpression.FromGuid(currentUserIdExctractedFromClaimsPrincipal);

            if (ChangeUpdateSetClause(column, newValue, updateCommand, out newClause, out existingSetClause))
            {
                autoSetClause.Add(newClause);
                if (existingSetClause != null)
                {
                    replacedSetClause.Add(existingSetClause);
                }
            }

            //Updated
            column = nameof(BaseEntity.EntityUpdated);
            newValue = DbExpression.FromDateTime(DateTime.Now);

            if (ChangeUpdateSetClause(column, newValue, updateCommand, out newClause, out existingSetClause))
            {
                autoSetClause.Add(newClause);
                if (existingSetClause != null)
                {
                    replacedSetClause.Add(existingSetClause);
                }
            }

            if (autoSetClause.Count > 0)
            {
                // Remove clauses
                var filteredSetClauses = updateCommand.SetClauses.Cast<DbSetClause>()
                    .Where(sc => !replacedSetClause.Contains(sc))
                    .ToList();
                Debug.Assert(filteredSetClauses.Count == updateCommand.SetClauses.Count - replacedSetClause.Count);

                //add new clauses
                filteredSetClauses.AddRange(autoSetClause);

                // Construct the final clauses, object representation of sql insert command values
                var finalUpdateSetClauses =
                    new ReadOnlyCollection<DbModificationClause>(new List<DbModificationClause>(filteredSetClauses));

                var newUpdateCommand = new DbInsertCommandTree(
                    updateCommand.MetadataWorkspace,
                    updateCommand.DataSpace,
                    updateCommand.Target,
                    finalUpdateSetClauses,
                    updateCommand.Returning);

                interceptionContext.Result = newUpdateCommand;
            }
        }

        private void InterceptInsertStatement(DbCommandTreeInterceptionContext interceptionContext,
            Guid currentUserIdExctractedFromClaimsPrincipal)
        {
            var insertCommand = interceptionContext.Result as DbInsertCommandTree;

            List<DbSetClause> replacedSetClause = new List<DbSetClause>();
            List<DbSetClause> autoSetClause = new List<DbSetClause>();

            //TENANT AWARE
            var column = nameof(ITenantAwareEntity.EntityTenantId);
            DbSetClause existingSetClause, newClause;
            DbExpression newValue = DbExpression.FromGuid(_multiTenantAccessFacilitator.GetCurrentTenantIdForDataInsertion());

            if (ChangeInsertSetClause(column, newValue, insertCommand, out newClause, out existingSetClause))
            {
                autoSetClause.Add(newClause);
                replacedSetClause.Add(existingSetClause);
            }

            //CreatedBy
            column = nameof(BaseEntity.EntityCreatedBy);
            newValue = DbExpression.FromGuid(currentUserIdExctractedFromClaimsPrincipal);
            if (ChangeInsertSetClause(column, newValue, insertCommand, out newClause, out existingSetClause))
            {
                autoSetClause.Add(newClause);
                replacedSetClause.Add(existingSetClause);
            }

            //Created
            column = nameof(BaseEntity.EntityCreated);
            newValue = DbExpression.FromDateTime(DateTime.Now);
            if (ChangeInsertSetClause(column, newValue, insertCommand, out newClause, out existingSetClause))
            {
                autoSetClause.Add(newClause);
                if (existingSetClause != null)
                {
                    replacedSetClause.Add(existingSetClause);
                }
            }

            Debug.Assert(autoSetClause.Count == replacedSetClause.Count);

            if (autoSetClause.Count > 0)
            {
                // Remove clauses
                var filteredSetClauses = insertCommand.SetClauses.Cast<DbSetClause>()
                    .Where(sc => !replacedSetClause.Contains(sc))
                    .ToList();
                Debug.Assert(filteredSetClauses.Count == insertCommand.SetClauses.Count - replacedSetClause.Count);

                //add new clauses
                filteredSetClauses.AddRange(autoSetClause);

                // Construct the final clauses, object representation of sql insert command values
                var finalSetClauses =
                    new ReadOnlyCollection<DbModificationClause>(new List<DbModificationClause>(filteredSetClauses));

                // construct a new returning
                var existingNewInstanceExpression = insertCommand.Returning as DbNewInstanceExpression;
                DbNewInstanceExpression newInstanceAfterInsert = null;
                if (existingNewInstanceExpression != null)
                {
                    var existingRowType = existingNewInstanceExpression.ResultType.EdmType as RowType;
                    //include existing.
                    var edmProperties = new List<EdmProperty>(existingRowType.Properties);

                    foreach (var dbSetClause in autoSetClause)
                    {
                        var propertyExpression = (dbSetClause.Property as DbPropertyExpression);
                        if (propertyExpression != null)
                        {
                            if (edmProperties.All(a => a.Name != propertyExpression.Property.Name))
                            {
                                var edmProperty = propertyExpression.Property.DeclaringType.Members
                                    .OfType<EdmProperty>()
                                    .First(p => p.Name == propertyExpression.Property.Name);
                                edmProperties.Add(edmProperty);
                            }
                        }
                    }
                    var rowType = RowType.Create(edmProperties, null);
                    List<DbExpression> arguments = new List<DbExpression>(existingNewInstanceExpression.Arguments);
                    foreach (var dbSetClause in autoSetClause)
                    {
                        var variableReference = DbExpressionBuilder.Variable(insertCommand.Target.VariableType,
                            insertCommand.Target.VariableName);
                        // Create the property to which will assign the correct value
                        var property = DbExpressionBuilder.Property(variableReference,
                            (dbSetClause.Property as DbPropertyExpression).Property.Name);

                        arguments.Add(property);
                    }


                    newInstanceAfterInsert =
                        DbExpressionBuilder.New(TypeUsage.Create(rowType, insertCommand.Returning.ResultType.Facets), arguments);
                }

                var newInsertCommand = new DbInsertCommandTree(
                    insertCommand.MetadataWorkspace,
                    insertCommand.DataSpace,
                    insertCommand.Target, finalSetClauses, newInstanceAfterInsert);

                interceptionContext.Result = newInsertCommand;
            }
        }

        private bool ChangeInsertSetClause(string column, DbExpression newValueToSetToDb, DbInsertCommandTree insertCommand, out DbSetClause newSetClause, out DbSetClause existingSetClause)
        {
            newSetClause = existingSetClause = null;
            existingSetClause = insertCommand.SetClauses.OfType<DbSetClause>().SingleOrDefault(p => (p.Property as DbPropertyExpression).Property.Name == column);



            if (existingSetClause != null)
            {
                // Create the variable reference in order to create the property
                var variableReference = DbExpressionBuilder.Variable(insertCommand.Target.VariableType,
                    insertCommand.Target.VariableName);
                // Create the property to which will assign the correct value
                var tenantProperty = DbExpressionBuilder.Property(variableReference, column);
                // Create the set clause, object representation of sql insert command
                newSetClause =
                    DbExpressionBuilder.SetClause(tenantProperty, newValueToSetToDb);
            }
            return newSetClause != null;
        }
        private bool ChangeUpdateSetClause(string column, DbExpression newValueToSetToDb, DbUpdateCommandTree updateCommand, out DbSetClause newSetClause, out DbSetClause existingSetClause)
        {
            newSetClause = existingSetClause = null;
            existingSetClause = updateCommand.SetClauses.OfType<DbSetClause>().SingleOrDefault(p => (p.Property as DbPropertyExpression).Property.Name == column);



            if (existingSetClause != null)
            {
                // Create the variable reference in order to create the property
                var variableReference = DbExpressionBuilder.Variable(updateCommand.Target.VariableType,
                    updateCommand.Target.VariableName);
                // Create the property to which will assign the correct value
                var tenantProperty = DbExpressionBuilder.Property(variableReference, column);
                // Create the set clause, object representation of sql insert command
                newSetClause =
                    DbExpressionBuilder.SetClause(tenantProperty, newValueToSetToDb);
            }
            return newSetClause != null;
        }
    }

1 个答案:

答案 0 :(得分:4)

在您的情况下,您可以简化代码,因为您使用Guids作为ID,因此无论租户如何,它们都是唯一的。

在插入中肯定可以使用原始返回命令(EF需要检索刚刚插入的实体的命令)。 我简化了删除更新和删除的代码。实际上我认为你也不需要它们(Guids仍然是独立于租户的独特之处,无论如何你可以恢复它们。)

我还添加了一个没有基类的类。 这里是代码

public class MyEntity
{
    public int Id { get; set; }
    [Required]
    [MaxLength(50)]
    public string Description { get; set; }

    [MaxLength(50)]
    public string TenantId { get; set; }
}

public class Context : DbContext
{
    private readonly IDbInterceptor _dbTreeInterceptor;

    public Context(DbConnection connection)
        : base(connection, false)
    {
        // NOT THE RIGHT PLACE TO DO THIS!!!
        _dbTreeInterceptor = new TenantCommandTreeInterceptor();
        DbInterception.Add(_dbTreeInterceptor);
    }

    public DbSet<MyEntity> MyEntities { get; set; }

    protected override void Dispose(bool disposing)
    {
        DbInterception.Remove(_dbTreeInterceptor);

        base.Dispose(disposing);
    }
}

public class TenantCommandTreeInterceptor : IDbCommandTreeInterceptor
{
    public void TreeCreated(DbCommandTreeInterceptionContext interceptionContext)
    {
        if (interceptionContext.OriginalResult.DataSpace != DataSpace.SSpace) return;

        if (interceptionContext.Result.CommandTreeKind == DbCommandTreeKind.Insert)
            InterceptInsertStatement(interceptionContext);
    }

    private void InterceptInsertStatement(DbCommandTreeInterceptionContext interceptionContext)
    {
        var insertCommand = interceptionContext.Result as DbInsertCommandTree;

        List<DbSetClause> replacedSetClause = new List<DbSetClause>();
        List<DbSetClause> autoSetClause = new List<DbSetClause>();

        //TENANT AWARE
        string column = "TenantId";
        DbSetClause existingSetClause;
        DbSetClause  newClause;
        DbExpression newValue = "JustMe"; // Here we should insert the right value

        if (ChangeInsertSetClause(column, newValue, insertCommand, out newClause, out existingSetClause))
        {
            autoSetClause.Add(newClause);
            replacedSetClause.Add(existingSetClause);
        }

        Debug.Assert(autoSetClause.Count == replacedSetClause.Count);

        if (autoSetClause.Count > 0)
        {
            // Remove clauses
            var filteredSetClauses = insertCommand.SetClauses.Cast<DbSetClause>()
                .Where(sc => !replacedSetClause.Contains(sc))
                .ToList();
            Debug.Assert(filteredSetClauses.Count == insertCommand.SetClauses.Count - replacedSetClause.Count);

            //add new clauses
            filteredSetClauses.AddRange(autoSetClause);

            // Construct the final clauses, object representation of sql insert command values
            var finalSetClauses =
                new ReadOnlyCollection<DbModificationClause>(new List<DbModificationClause>(filteredSetClauses));

            // In insert probably you can avoid to change the newInstanceAfterInsert because you are using a Guid for the entity ID that is always unique (it does not matter the tenant). 

            var newInsertCommand = new DbInsertCommandTree(
                insertCommand.MetadataWorkspace,
                insertCommand.DataSpace,
                insertCommand.Target, 
                finalSetClauses, 
                insertCommand.Returning);

            interceptionContext.Result = newInsertCommand;
        }
    }

    private bool ChangeInsertSetClause(string column, DbExpression newValueToSetToDb, DbInsertCommandTree insertCommand, out DbSetClause newSetClause, out DbSetClause existingSetClause)
    {
        newSetClause = null;
        existingSetClause = insertCommand.SetClauses.
            OfType<DbSetClause>().SingleOrDefault(p => (p.Property as DbPropertyExpression).Property.Name == column);



        if (existingSetClause != null)
        {
            // Create the variable reference in order to create the property
            var variableReference = DbExpressionBuilder.Variable(insertCommand.Target.VariableType,
                insertCommand.Target.VariableName);
            // Create the property to which will assign the correct value
            var tenantProperty = DbExpressionBuilder.Property(variableReference, column);
            // Create the set clause, object representation of sql insert command
            newSetClause =
                DbExpressionBuilder.SetClause(tenantProperty, newValueToSetToDb);
        }
        return newSetClause != null;
    }
}


static class Test
{
    public static void Run(DbConnection connection)
    {

        using (Context context = new Context(connection))
        {
            context.MyEntities.Add(new MyEntity()
            {
                Description = "My first message"
            });
            context.SaveChanges();
        }



    }
}

调用Test.Run这是EF针对数据库运行的查询。

insert into [MyEntities]([Description], [TenantId])
values (@p0, @p1);
select [Id]
from [MyEntities]
where [Id] = @@identity
@p0 = My first message
@p1 = JustMe

使用Id和TenantId创建基类也可以。

所以,在你的情况下,最好的事情就是你开始简单地按照我的方式添加你需要的部分。

修改 这可以工作,但插入的实体不会使用TenantId更新。

要解决此问题,您可以将字段设置为数据库生成(因此EF将读取它),但在拦截器中生成它。
这里是示例,但要运行它,您需要修复TODO:
如果您想测试它,可以设置断点并避免在迁移表中拦截插入。

public class MyEntity
{
    public int Id { get; set; }

    [Required]
    [MaxLength(50)]
    public string Description { get; set; }

    [MaxLength(50)]
    [DatabaseGenerated(DatabaseGeneratedOption.Computed)]
    public string TenantId { get; set; }

}

public class Context : DbContext
{
    private readonly IDbInterceptor _dbTreeInterceptor;

    public Context(DbConnection connection)
        : base(connection, false)
    {
        // NOT THE RIGHT PLACE TO DO THIS!!!
        _dbTreeInterceptor = new TenantCommandTreeInterceptor();
        DbInterception.Add(_dbTreeInterceptor);
    }

    public DbSet<MyEntity> MyEntities { get; set; }

    protected override void Dispose(bool disposing)
    {
        DbInterception.Remove(_dbTreeInterceptor);

        base.Dispose(disposing);
    }
}




public class TenantCommandTreeInterceptor : IDbCommandTreeInterceptor
{
    public void TreeCreated(DbCommandTreeInterceptionContext interceptionContext)
    {
        if (interceptionContext.OriginalResult.DataSpace != DataSpace.SSpace) return;

        if (interceptionContext.Result.CommandTreeKind == DbCommandTreeKind.Insert)
            InterceptInsertStatement(interceptionContext);
    }

    private void InterceptInsertStatement(DbCommandTreeInterceptionContext interceptionContext)
    {
        var insertCommand = interceptionContext.Result as DbInsertCommandTree;

        List<DbModificationClause> finalSetClauses = new List<DbModificationClause>((IEnumerable<DbModificationClause>)insertCommand.SetClauses);

        //TENANT AWARE
        string column = "TenantId";
        DbExpression newValue = "JustMe"; // Here we should insert the right value


        // TODO: Need to check if this entity is a Multitenant entity in the right way
        // You can use the attribute like in the original sample


        finalSetClauses.Add(
            GetInsertSetClause(column, newValue, insertCommand));

        // Construct the final clauses, object representation of sql insert command values

        // In insert probably you can avoid to change the newInstanceAfterInsert because you are using a Guid for the entity ID that is always unique (it does not matter the tenant). 

        var newInsertCommand = new DbInsertCommandTree(
            insertCommand.MetadataWorkspace,
            insertCommand.DataSpace,
            insertCommand.Target,
            new ReadOnlyCollection<DbModificationClause>(finalSetClauses),
            insertCommand.Returning);

        interceptionContext.Result = newInsertCommand;
    }

    private DbSetClause GetInsertSetClause(string column, DbExpression newValueToSetToDb, DbInsertCommandTree insertCommand)
    {
        // Create the variable reference in order to create the property
        DbVariableReferenceExpression variableReference = DbExpressionBuilder.Variable(insertCommand.Target.VariableType,
            insertCommand.Target.VariableName);
        // Create the property to which will assign the correct value
        DbPropertyExpression tenantProperty = DbExpressionBuilder.Property(variableReference, column);
        // Create the set clause, object representation of sql insert command
        DbSetClause newSetClause = DbExpressionBuilder.SetClause(tenantProperty, newValueToSetToDb);
        return newSetClause;
    }
}

    public static void Run(DbConnection connection)
    {

        using (Context context = new Context(connection))
        {
            MyEntity myNewEntity;

            context.MyEntities.Add(myNewEntity = new MyEntity()
            {
                Description = "My first message"
            });
            context.SaveChanges();

            Console.WriteLine("{0} {1}", myNewEntity.Id, myNewEntity.TenantId);

        }



    }