Mutar el árbol de expresión de un predicado para apuntar a otro tipo
En la aplicación en la que estoy trabajando actualmente, hay dos tipos de cada objeto comercial: el tipo "ActiveRecord" y el tipo "DataContract". Entonces, por ejemplo, habría:
namespace ActiveRecord {
class Widget {
public int Id { get; set; }
}
}
namespace DataContract {
class Widget {
public int Id { get; set; }
}
}
La capa de acceso a la base de datos se encarga de traducir entre familias: puede indicarle que actualice unDataContract.Widget
y mágicamente creará unActiveRecord.Widget
con los mismos valores de propiedad y guárdelo en su lugar.
El problema surgió al intentar refactorizar esta capa de acceso a la base de datos.
El problemaQuiero agregar métodos como el siguiente a la capa de acceso a la base de datos:
// Widget is DataContract.Widget
interface IDbAccessLayer {
IEnumerable<Widget> GetMany(Expression<Func<Widget, bool>> predicate);
}
Lo anterior es un método simple "get" de uso general con predicado personalizado. El único punto de interés es que estoy pasando un árbol de expresión en lugar de un lambda porque adentroIDbAccessLayer
Estoy consultando unIQueryable<ActiveRecord.Widget>
; para hacerlo de manera eficiente (piense en LINQ to SQL) necesito pasar un árbol de expresión para que este método pida eso.
El inconveniente: el parámetro debe transformarse mágicamente de unExpression<Func<DataContract.Widget, bool>>
a unaExpression<Func<ActiveRecord.Widget, bool>>
.
Lo que me gustaría hacer adentroGetMany
es:
IEnumerable<DataContract.Widget> GetMany(
Expression<Func<DataContract.Widget, bool>> predicate)
{
var lambda = Expression.Lambda<Func<ActiveRecord.Widget, bool>>(
predicate.Body,
predicate.Parameters);
// use lambda to query ActiveRecord.Widget and return some value
}
Esto no funcionará porque en un escenario típico, por ejemplo si:
predicate == w => w.Id == 0;
... el árbol de expresión contiene unMemberAccessExpression
instancia que tiene una propiedad de tipoMemberInfo
eso describeDataContract.Widget.Id
. También hayParameterExpression
instancias tanto en el árbol de expresión como en su colección de parámetros (predicate.Parameters
) que describenDataContract.Widget
; todo esto generará errores, ya que el cuerpo consultable no contiene ese tipo de widget, sino más bienActiveRecord.Widget
.
Después de buscar un poco, encontréSystem.Linq.Expressions.ExpressionVisitor
(su fuente se puede encontraraquí en el contexto de un tutorial), que ofrece una forma conveniente de modificar un árbol de expresión. En .NET 4, esta clase se incluye fuera de la caja.
Armado con esto, implementé un visitante. Este simple visitante solo se encarga de cambiar los tipos en el acceso de miembros y las expresiones de parámetros, pero eso es suficiente funcionalidad para trabajar con el predicadow => w.Id == 0
.
internal class Visitor : ExpressionVisitor
{
private readonly Func<Type, Type> typeConverter;
public Visitor(Func<Type, Type> typeConverter)
{
this.typeConverter = typeConverter;
}
protected override Expression VisitMember(MemberExpression node)
{
var dataContractType = node.Member.ReflectedType;
var activeRecordType = this.typeConverter(dataContractType);
var converted = Expression.MakeMemberAccess(
base.Visit(node.Expression),
activeRecordType.GetProperty(node.Member.Name));
return converted;
}
protected override Expression VisitParameter(ParameterExpression node)
{
var dataContractType = node.Type;
var activeRecordType = this.typeConverter(dataContractType);
return Expression.Parameter(activeRecordType, node.Name);
}
}
Con este visitanteGetMany
se convierte en:
IEnumerable<DataContract.Widget> GetMany(
Expression<Func<DataContract.Widget, bool>> predicate)
{
var visitor = new Visitor(...);
var lambda = Expression.Lambda<Func<ActiveRecord.Widget, bool>>(
visitor.Visit(predicate.Body),
predicate.Parameters.Select(p => visitor.Visit(p));
var widgets = ActiveRecord.Widget.Repository().Where(lambda);
// This is just for reference, see below
Expression<Func<ActiveRecord.Widget, bool>> referenceLambda =
w => w.Id == 0;
// Here we 'd convert the widgets to instances of DataContract.Widget and
// return them -- this has nothing to do with the question though.
}
ResultadosLa buena noticia es quelambda
Está construido muy bien. La mala noticia es que no está funcionando; me está explotando cuando trato de usarlo, y los mensajes de excepción realmente no son útiles en absoluto.
He examinado la lambda que produce mi código y una lambda codificada con la misma expresión; Se ven exactamente iguales. Pasé horas en el depurador tratando de encontrar alguna diferencia, pero no puedo.
Cuando el predicado esw => w.Id == 0
, lambda
se ve exactamente comoreferenceLambda
. Pero este último funciona con p.IQueryable<T>.Where
, mientras que el primero no; He intentado esto en la ventana inmediata del depurador.
También debo mencionar que cuando el predicado esw => true
Todo funciona bien. Por lo tanto, supongo que no estoy haciendo suficiente trabajo en el visitante, pero no puedo encontrar más pistas para seguir.
Después de tener en cuenta las respuestas correctas al problema (dos de ellas a continuación; una breve, una con código), el problema se resolvió; Puse el código junto con algunas notas importantes en unrespuesta separada para evitar que esta larga pregunta se vuelva aún más larga.
¡Gracias a todos por sus respuestas y comentarios!