Добавление внутреннего соединения в DbScanExpression в Entity Framework Interceptor
Я пытаюсь использовать перехватчик Entity Framework CommandTree, чтобы добавить фильтр к каждому запросу через DbContext.
Для простоты у меня есть две таблицы, одна из которых называется "Пользователь" с двумя столбцами ( "UserId" и "EmailAddress" ), а другая называется "TenantUser" с двумя столбцами ( "UserId" и "TenantId" ).
Каждый раз, когда есть таблица DbScan для пользователя, я хочу сделать внутреннее соединение с таблицей TenantUser и фильтровать на основе столбца TenantId.
Существует проект под названием EntityFramework.Filters, который делает что-то в этом направлении, но не поддерживает "сложные объединения", что, кажется, является тем, что я пытаясь сделать.
Следуя демо от TechEd 2014, я создал перехватчик, который использует посетителя с приведенным ниже способом для замены DbScanExpressions на DbJoinExpression. Как только я получу эту работу, я планирую обернуть ее в DbFilterExpression, чтобы сравнить столбец TenantId с известным идентификатором.
public override DbExpression Visit(DbScanExpression expression)
{
var table = expression.Target.ElementType as EntityType;
if (table != null && table.Name == "User")
{
return DbExpressionBuilder.InnerJoin(expression, DbExpressionBuilder.Scan(expression.Target), (l, r) =>
DbExpressionBuilder.Equal(DbExpressionBuilder.Variable(tenantUserIdProperty.TypeUsage, "UserId"),
DbExpressionBuilder.Variable(userIdProperty.TypeUsage, "UserId")));
}
return base.Visit(expression);
}
Чтобы проверить код выше, я добавил перехватчик в dbContext и запустил следующий код:
dbContext.Users.Select(u => new { u.EmailAddress }).ToList();
Однако это приводит к следующей ошибке:
Нет свойства с именем "EmailAddress" объявляется типом "Transient.rowtype [(l, CodeFirstDatabaseSchema.User(Nullable = True, DefaultValue =)), (r, CodeFirstDatabaseSchema.User(Nullable = True, DefaultValue = ))]".
Я неправильно создаю DbJoinExpression? Или я пропущу что-то еще?
Ответы
Ответ 1
Причина, по которой вы получили это исключение, состоит в том, что InnerJoin производит результат, объединенный из столбцов из обеих таблиц, а с другой стороны запрос должен возвращать соответствующие свойства класса User, поэтому вам дополнительно нужно использовать проекцию в конце запрос. Вот код, который работал у меня:
public override DbExpression Visit(DbScanExpression expression)
{
var table = expression.Target.ElementType as EntityType;
if (table != null && table.Name == "User")
{
return expression.InnerJoin(
DbExpressionBuilder.Scan(expression.Target.EntityContainer.BaseEntitySets.Single(s => s.Name == "TennantUser")),
(l, r) =>
DbExpressionBuilder.Equal(
DbExpressionBuilder.Property(l, "UserId"),
DbExpressionBuilder.Property(r, "UserId")
)
)
.Select(exp =>
new {
UserId = exp.Property("l").Property("UserId"),
Email = exp.Property("l").Property("Email")
});
}
return base.Visit(expression);
}
Как вы видите после операции соединения, вы ссылаетесь на конкретную связанную таблицу, используя свой псевдоним выражения лямбда из выражения, определяющего условие соединения. Поэтому в моем случае вы ссылаетесь на таблицу пользователя как l и на TennantUser как r. Буквы l и r будут использоваться, а также псевдонимы в результате SQL-запроса, отправленного в базу данных. Между операциями InnerJoin и Select вы можете добавить дополнительную логику, например Filter и т.д.