【发布时间】:2016-02-17 14:18:42
【问题描述】:
根据我找到的一篇文章here,我实现了一个多租户 IDbCommandInterceptor。 我注意到该实现不会更新我作为 IDbCommandInterceptor 的一部分插入的模型上的值。我希望能解决这个问题。
我使用拦截器为基础模型上的以下属性添加正确的值:
- EntityTenantId
- 已创建
- 创建者
此外,我将 DbInsertCommandTree 更改为使用包含这些字段的 DbNewInstanceExpression,除了 EntityId(身份)和时间戳。
尽管我添加的 3 个额外字段被正确查询,但模型中仅更新了数据库生成的字段(EntityId 和时间戳)。
问题: 是否有人知道如何让 EF 在插入后更新我的模型,以获取通常不属于生成的数据库类别的字段? 换句话说,在调用 SaveChanges() 保存新实体后,该实体将更新 EntityId 和 Timestamp 的属性值。如何确保模型实例上的 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;
}
}
【问题讨论】:
-
@bubi 感谢您抽出宝贵的时间。添加拦截器代码
标签: .net sql-server entity-framework code-first