根据我发现here的文章,我实现了一个多租户IDbCommandInterceptor。 我注意到实现不会更新我作为IDbCommandInterceptor的一部分插入的模型上的值。我希望解决这个问题。
我使用拦截器为基本模型上的以下属性添加正确的值:
此外,我更改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;
}
}
答案 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);
}
}