Intro
Dans l'application que je suis en train de travailler, il y a deux sortes de chaque objet métier: le "ActiveRecord" genre " et de la "DataContract" genre". Ainsi, par exemple, il y aurait:
namespace ActiveRecord {
class Widget {
public int Id { get; set; }
}
}
namespace DataContract {
class Widget {
public int Id { get; set; }
}
}
La base de données de la couche d'accès prend en charge la conversion entre les familles: vous pouvez le dire à mettre à jour un DataContract.Widget
et il va automatiquement créer un ActiveRecord.Widget
avec les mêmes valeurs de propriété et d'économiser de la place.
Le problème a fait surface lors de la tentative de refactoriser cette base de données de la couche d'accès.
Le Problème
Je veux ajouter des méthodes comme la suivante dans la base de données de la couche d'accès:
// Widget is DataContract.Widget
interface IDbAccessLayer {
IEnumerable<Widget> GetMany(Expression<Func<Widget, bool>> predicate);
}
Le dessus est un simple usage général la méthode "get" à la coutume de prédicat. Le seul point d'intérêt, c'est que je suis de passage dans une arborescence d'expression au lieu d'une lambda parce qu'à l'intérieur IDbAccessLayer
je suis interrogation d'un IQueryable<ActiveRecord.Widget>
; pour le faire efficacement (pensez à LINQ to SQL) j'ai besoin de passer d'une expression à l'arbre, de sorte que cette méthode demande pour cela.
Le hic: le paramètre doit être transformée comme par magie à partir d'un Expression<Func<DataContract.Widget, bool>>
d'un Expression<Func<ActiveRecord.Widget, bool>>
.
Tentative De Solution
Ce que j'aimerai faire à l'intérieur d' GetMany
est:
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
}
Cela ne marchera pas, parce que dans un scénario typique, par exemple, si:
predicate == w => w.Id == 0;
...l'expression de l'arbre contient un MemberAccessExpression
instance, qui est une propriété de type MemberInfo
qui décrit DataContract.Widget.Id
.
Il y a aussi ParameterExpression
instances à la fois dans l'expression de l'arbre et de son paramètre de collecte (predicate.Parameters
) qui décrivent DataContract.Widget
; ce qui entraînera des erreurs depuis le queryable corps ne contient pas ce type de widget, mais plutôt ActiveRecord.Widget
.
Après avoir cherché un peu, j'ai trouvé System.Linq.Expressions.ExpressionVisitor
(de sa source peuvent être trouvés ici dans le contexte d'un how-to), qui offre un moyen pratique pour modifier une expression de l'arbre. Dans .NET 4, cette classe est incluse hors de la boîte.
Armé avec cela, j'ai mis en place un visiteur. Cette simple visiteur ne s'occupe que de changer de types d'accès membres et les paramètres des expressions, mais c'est suffisamment de fonctionnalités pour travailler avec le prédicat w => 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);
}
}
Avec ce visiteur, GetMany
devient:
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.
}
Résultats
La bonne nouvelle, c'est qu' lambda
est construit à l'amende juste. La mauvaise nouvelle est qu'il n'est pas de travail, c'est de souffler sur moi quand j'essaie de l'utiliser, et à l'exception des messages sont vraiment pas utiles à tous.
J'ai examiné attentivement la lambda mon code produit et une codé en dur lambda avec la même expression; ils regardent exactement la même. J'ai passé des heures dans le débogueur essayant de trouver une différence, mais je ne peux pas.
Lorsque le prédicat est - w => w.Id == 0
, lambda
ressemble referenceLambda
. Mais ce dernier travaille avec, par exemple, IQueryable<T>.Where
, tandis que le premier n'a pas; j'ai essayé ceci dans la fenêtre du débogueur.
Je devrais aussi mentionner que, lorsque le prédicat est - w => true
, tout fonctionne bien. Donc je suis en supposant que je ne suis pas du faire assez de travail dans le visiteur, mais je ne trouve pas plus de prospects à suivre.
La Solution Finale
Après avoir pris en compte les réponses correctes pour le problème (deux d'entre eux ci-dessous; l'un court, l'une avec code) le problème a été résolu; j'ai mis le code avec quelques notes importantes dans une réponse distincte de garder cette longue question de devenir encore plus longtemps.
Merci à tous pour vos réponses et vos commentaires!